O. *_*per 12 .net c# wpf panel layoutpanels
tl; dr:我想重用预定义WPF面板的现有布局逻辑来实现自定义WPF面板类.这个问题包含四种不同的尝试来解决这个问题,每个尝试都有不同的缺点,因而有不同的失败点.此外,可以进一步找到一个小测试用例.
问题是:我如何正确实现这一目标
我正在尝试编写自定义WPF 面板.对于这个面板类,我想坚持推荐的开发实践并维护一个干净的API和内部实现.具体地说,这意味着:
至于目前,我将密切关注现有的布局,我想重新使用另一个面板的布局代码(而不是再次编写布局代码,如此处所示).为了举个例子,我将基于解释DockPanel,虽然我想知道如何做到这一点,基于任何一种Panel.
为了重用布局逻辑,我打算DockPanel在我的面板中添加一个可视子项,然后保存并布局我的面板的逻辑子项.
我已经尝试了三种不同的想法来解决这个问题,并且在评论中提出了另一个想法,但到目前为止,每个想法都在不同的点上失败了:
1)在自定义面板的控件模板中引入内部布局面板
这似乎是最优雅的解决方案 - 这样,自定义面板的控制面板可以使用ItemsControl其ItemsPanel属性使用a DockPanel,其ItemsSource属性绑定到自定义面板的Children属性.
不幸的是,Panel不继承Control,因此没有Template属性,也没有对控件模板的功能支持.
另一方面,Children属性是由引入的Panel,因此不存在Control,我觉得打破预期的继承层次结构并创建一个实际上是a Control但不是a 的面板可能会被认为是hacky Panel.
2)提供我的面板的子列表,该列表仅仅是内部面板的子列表的包装器
这样的课程如下所示.我已经子类UIElementCollection在我的面板类,并从一个重写版本返回它CreateUIElementCollection的方法.(我只复制了这里实际调用的方法;我已经实现了其他方法来抛出一个NotImplementedException,所以我确信没有调用其他可覆盖的成员.)
using System;
using System.Windows;
using System.Windows.Controls;
namespace WrappedPanelTest
{
public class TestPanel1 : Panel
{
private sealed class ChildCollection : UIElementCollection
{
public ChildCollection(TestPanel1 owner) : base(owner, owner)
{
if (owner == null) {
throw new ArgumentNullException("owner");
}
this.owner = owner;
}
private readonly TestPanel1 owner;
public override int Add(System.Windows.UIElement element)
{
return this.owner.innerPanel.Children.Add(element);
}
public override int Count {
get {
return owner.innerPanel.Children.Count;
}
}
public override System.Windows.UIElement this[int index] {
get {
return owner.innerPanel.Children[index];
}
set {
throw new NotImplementedException();
}
}
}
public TestPanel1()
{
this.AddVisualChild(innerPanel);
}
private readonly DockPanel innerPanel = new DockPanel();
protected override UIElementCollection CreateUIElementCollection(System.Windows.FrameworkElement logicalParent)
{
return new ChildCollection(this);
}
protected override int VisualChildrenCount {
get {
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index)
{
if (index == 0) {
return innerPanel;
} else {
throw new ArgumentOutOfRangeException();
}
}
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
innerPanel.Measure(availableSize);
return innerPanel.DesiredSize;
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
innerPanel.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
}
}
Run Code Online (Sandbox Code Playgroud)
这几乎是正确的; 在DockPanel预期布局重用.唯一的问题是绑定没有按名称(使用ElementName属性)在面板中找到控件.
我试过从LogicalChildren房产中归还了内心的孩子,但这并没有改变任何东西:
protected override System.Collections.IEnumerator LogicalChildren {
get {
return innerPanel.Children.GetEnumerator();
}
}
Run Code Online (Sandbox Code Playgroud)
在用户Arie的回答中,该类被指出在这方面起着至关重要的作用:由于某种原因,子控件的名称没有在相关中注册.这可以通过调用每个子节点来部分修复,但是需要检索正确的实例.此外,我不确定例如,子项名称更改时的行为是否与其他面板中的行为相同.NameScopeNameScopeRegisterNameNameScope
相反,设置NameScope内部面板似乎是要走的路.我试着用一个简单的绑定(在TestPanel1构造函数中):
BindingOperations.SetBinding(innerPanel,
NameScope.NameScopeProperty,
new Binding("(NameScope.NameScope)") {
Source = this
});
Run Code Online (Sandbox Code Playgroud)
不幸的是,这只是将NameScope内部面板设置为null.据我所知,通过Snoop,实际的NameScope实例只存储在父窗口的NameScope附加属性中,或者由控件模板定义的封闭可视树的根(或者可能由某个其他关键节点? ),不管是什么类型.当然,可以在控制树的生命周期中在控制树中的不同位置添加和移除控件实例,因此相关NameScope可能会不时地改变.这再次要求具有约束力.
这是我再次陷入困境的地方,因为遗憾的是,无法RelativeSource根据任意条件定义绑定,例如*第一个遇到的节点,该节点具有null分配给NameScope附加属性的非值.
除非关于如何对周围可视化树中的更新作出反应的另一个问题产生有用的响应,否则有更好的方法来检索和/或绑定到NameScope任何给定框架元素的当前相关性?
3)使用内部面板,其子列表与外部面板的实例完全相同
不是将子列表保留在内部面板中并将调用转发到外部面板的子列表,而是反过来.这里,只使用外部面板的子列表,而内部面板从不创建自己的子列表,而只是使用相同的实例:
using System;
using System.Windows;
using System.Windows.Controls;
namespace WrappedPanelTest
{
public class TestPanel2 : Panel
{
private sealed class InnerPanel : DockPanel
{
public InnerPanel(TestPanel2 owner)
{
if (owner == null) {
throw new ArgumentNullException("owner");
}
this.owner = owner;
}
private readonly TestPanel2 owner;
protected override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent)
{
return owner.Children;
}
}
public TestPanel2()
{
this.innerPanel = new InnerPanel(this);
this.AddVisualChild(innerPanel);
}
private readonly InnerPanel innerPanel;
protected override int VisualChildrenCount {
get {
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index)
{
if (index == 0) {
return innerPanel;
} else {
throw new ArgumentOutOfRangeException();
}
}
protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
{
innerPanel.Measure(availableSize);
return innerPanel.DesiredSize;
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
{
innerPanel.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
}
}
Run Code Online (Sandbox Code Playgroud)
在这里,按名称布局和绑定到控件有效.但是,控件不可单击.
我怀疑我必须以某种方式转移呼叫到HitTestCore(GeometryHitTestParameters)和HitTestCore(PointHitTestParameters)内板.但是,在内部面板中,我只能访问InputHitTest,所以我既不确定如何安全地处理原始HitTestResult实例而不丢失或忽略原始实现所尊重的任何信息,也不知道如何处理GeometryHitTestParameters,因为InputHitTest只接受简单Point.
此外,控制也无法集中,例如通过按压Tab.我不知道如何解决这个问题.
此外,我有点担心这样做,因为我不确定内部面板和原始的孩子列表之间的内部链接我打破了用自定义对象替换子列表.
4)直接从面板类继承
用户Clemens建议直接让我的类继承DockPanel.但是,有两个原因可以解释为什么这不是一个好主意:
DockPanel.但是,有可能在将来的某个时候,这还不够,有人确实必须在我的面板中编写自定义布局逻辑.在这种情况下,用DockPanel自定义布局代码替换内部是微不足道的,但是DockPanel从我的面板的继承层次结构中删除将意味着一个重大变化.DockPanel,那么面板的用户可能会通过弄乱DockPanel特定的属性来破坏其布局代码LastChildFill.虽然它只是那个属性,但我想使用一种适用于所有Panel子类型的方法.例如,从中派生的自定义面板Grid将公开ColumnDefinitions和RowDefinitions属性,通过该属性可以通过自定义面板的公共接口完全销毁任何自动生成的布局.作为观察所述问题的测试用例,在XAML中添加要测试的自定义面板的实例,并在该元素中添加以下内容:
<TextBox Name="tb1" DockPanel.Dock="Right"/>
<TextBlock Text="{Binding Text, ElementName=tb1}" DockPanel.Dock="Left"/>
Run Code Online (Sandbox Code Playgroud)
文本框应留在文本框的左侧,并且应显示当前在文本框中写入的内容.
我希望文本框是可点击的,输出视图不显示任何绑定错误(因此,绑定也应该工作).
因此,我的问题是:
如果第二种方法的唯一问题(提供我的面板的子列表,它只是内部面板的子列表的包装)是缺乏按名称绑定到内部面板的控件的能力,那么解决方案是:
public DependencyObject this[string childName]
{
get
{
return innerPanel.FindChild<DependencyObject>(childName);
}
}
Run Code Online (Sandbox Code Playgroud)
然后是示例绑定:
"{Binding ElementName=panelOwner, Path=[innerPanelButtonName].Content}"
Run Code Online (Sandbox Code Playgroud)
FindChild方法的实现: /sf/answers/123194641/
编辑:
如果您希望ElementName 的“通常”绑定起作用,则必须在相应的NameScope中注册作为 innerPanel 子级的控件的名称:
var ns = NameScope.GetNameScope(Application.Current.MainWindow);
foreach (FrameworkElement child in innerPanel.Children)
{
ns.RegisterName(child.Name, child);
}
Run Code Online (Sandbox Code Playgroud)
现在绑定{Binding ElementName=innerPanelButtonName, Path=Content}将在运行时起作用。
这样做的问题是可靠地找到根 UI 元素来获取NameScope(此处:Application.Current.MainWindow- 在设计时不起作用)
OP 编辑:这个答案让我走上了正轨,因为它提到了NameScope课程。
我的最终解决方案基于并使用接口TestPanel1的自定义实现。它的每个方法都从外部面板开始沿着逻辑树向上查找,以查找属性不是的最近的父元素:INameScopeNameScopenull
RegisterName并将UnregisterName它们的调用转发到找到的对象的相应方法INameScope,否则抛出异常。FindName将其调用转发到FindName找到的INameScope对象,否则(如果没有找到这样的对象)返回null。该INameScope实现的一个实例被设置为NameScope内部面板的实例。