如何为新的Panel Class重用现有的Layouting Code?

tl; dr:我想重用自定义WPF面板类的预定义WPF面板的现有布局逻辑。 这个问题包含四种不同的尝试来解决这个问题,每个尝试都有不同的缺点,因而有不同的失败点。 此外,还可以找到一个小型测试用例。

问题是:我如何正确实现这一目标

  • 定义我的自定义面板
  • 内部重用另一个面板的布局逻辑,没有
  • 遇到我试图解决这个问题所描述的问题?

我正在尝试编写自定义WPF 面板 。 对于这个面板类,我想坚持推荐的开发实践并维护一个干净的API和内部实现。 具体地说,这意味着:

  • 我想避免复制和粘贴代码; 如果代码的几个部分具有相同的function,则代码应该只存在一次并重复使用。
  • 我想应用适当的封装,让外部用户只能访问可以安全使用的成员(不破坏任何内部逻辑,或者不泄露任何内部实现特定的信息)。

至于目前,我将密切关注现有的布局,我想重新使用另一个面板的布局代码(而不是再次编写布局代码,如此处所示 )。 为了举个例子,我将基于DockPanel解释,虽然我想知道如何基于任何类型的Panel

为了重用布局逻辑,我打算在我的面板中添加一个DockPanel作为可视子项,然后它将保持并布局我的面板的逻辑子项。

我已经尝试了三种不同的想法如何解决这个问题,并在评论中提出了另一个想法,但到目前为止,每个想法都在不同的点上失败了:


1)在自定义面板的控件模板中引入内部布局面板

这似乎是最优雅的解决方案 – 这样,自定义面板的控制面板可以使用ItemsControlItemsPanel属性使用DockPanel ,其ItemsSource属性绑定到自定义面板的Children属性 。

不幸的是, Panel不从Controlinheritance,因此没有Template属性 ,也没有控件模板的function支持。

另一方面, Children属性是由Panel引入的,因此不会出现在Control ,我觉得打破预期的inheritance层次结构并创建一个实际上是ControlPanel ,而不是Panel ,可能会被认为是hacky 。


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; } } } 

几乎是正确的; DockPanel布局按预期重用。 唯一的问题是绑定没有按名称在面板中找到控件(使用ElementName属性 )。

我试过从LogicalChildren属性返回内部子项,但这并没有改变任何东西:

 protected override System.Collections.IEnumerator LogicalChildren { get { return innerPanel.Children.GetEnumerator(); } } 

在用户Arie的回答中 , NameScope类被指出在这方面起着至关重要的作用:由于某种原因,子控件的名称没有在相关的NameScope注册。 这可以通过为每个子节点调用RegisterName来部分修复,但是需要检索正确的NameScope实例。 此外,我不确定例如,孩子的名字改变时的行为是否与其他小组中的行为相同。

相反,设置内部面板的NameScope似乎是要走的路。 我尝试使用简单的绑定(在TestPanel1构造函数中):

  BindingOperations.SetBinding(innerPanel, NameScope.NameScopeProperty, new Binding("(NameScope.NameScope)") { Source = this }); 

不幸的是,这只是将内部面板的NameScope设置为null 。 据我所知,通过Snoop ,实际的NameScope实例只存储在父窗口的NameScope附加属性中,或者由控件模板定义的封闭可视树的根(或者可能由其他一些键定义)节点?),无论什么类型。 当然,可以在控制树的生命周期中在控制树中的不同位置添加和删除控件实例,因此相关的NameScope可能会不时更改。 这再次要求具有约束力。

这是我再次陷入困境的地方,因为遗憾的是,无法根据任意条件为绑定定义RelativeSource ,例如*第一个遇到的节点,该节点具有分配给NameScope附加属性的非null值。

除非关于如何对周围可视化树中的更新作出反应的另一个问题产生有用的响应,是否有更好的方法来检索和/或绑定当前与任何给定框架元素相关的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; } } } 

在这里,按名称布局和绑定控件有效。 但是,控件不可单击。

我怀疑我必须以某种方式HitTestCore(PointHitTestParameters) HitTestCore(GeometryHitTestParameters)HitTestCore(PointHitTestParameters)调用转发到内部面板。 但是,在内部面板中,我只能访问InputHitTest ,因此我既不确定如何安全地处理原始HitTestResult实例而不会丢失或忽略原始实现所InputHitTest任何信息,也不知道如何处理GeometryHitTestParameters ,如InputHitTest只接受一个简单的Point

此外,控件也不可调焦,例如按Tab键 。 我不知道如何解决这个问题。

此外,我对这种方式略显警惕,因为我不确定内部面板和原始的孩子列表之间的内部链接是什么,我打破了用自定义对象替换子列表。


4)直接从面板类inheritance

用户Clemens建议直接让我的类inheritance自DockPanel 。 但是,有两个原因可以解释为什么这不是一个好主意:

  • 我面板的当前版本将依赖于DockPanel的布局逻辑。 但是,有可能在将来的某个时候,这将是不够的,并且有人确实必须在我的面板中编写自定义布局逻辑。 在这种情况下,用自定义布局代码替换内部DockPanel是微不足道的,但是从我的面板的inheritance层次结构中删除DockPanel意味着一个重大变化。
  • 如果我的面板inheritance自DockPanel ,那么面板的用户可能会破坏DockPanel公开的属性,特别是LastChildFill ,从而破坏其布局代码。 虽然它只是那个属性,但我想使用一种适用于所有Panel子类型的方法。 例如,从Grid派生的自定义面板将公开ColumnDefinitionsRowDefinitions属性,通过这些属性可以通过自定义面板的公共接口完全销毁任何自动生成的布局。

作为观察所述问题的测试用例,在XAML中添加要测试的自定义面板的实例,并在该元素中添加以下内容:

   

文本块应留在文本框中,并且应显示当前在文本框中写入的内容。

我希望文本框是可点击的,输出视图不显示任何绑定错误(因此,绑定也应该工作)。


因此,我的问题是:

  • 我的任何一次尝试都可以修复,以获得完全正确的解决方案吗? 或者是否有一种完全不同于我试图做的事情的其他方式?

如果您的第二种方法的唯一问题(提供我的面板的子列表仅仅是内部面板的子列表的包装)是缺乏按名称绑定到内部面板控件的能力,那么解决方案将是:

  public DependencyObject this[string childName] { get { return innerPanel.FindChild(childName); } } 

然后,示例绑定:

 "{Binding ElementName=panelOwner, Path=[innerPanelButtonName].Content}" 

FindChild方法的实现: https ://stackoverflow.com/a/1759923/891715


编辑:

如果希望ElementName的“通常” 绑定起作用,则必须在相应的NameScope中 注册作为innerPanel的子控件的控件的名称 :

 var ns = NameScope.GetNameScope(Application.Current.MainWindow); foreach (FrameworkElement child in innerPanel.Children) { ns.RegisterName(child.Name, child); } 

现在绑定{Binding ElementName=innerPanelButtonName, Path=Content}将在运行时工作。

这个问题是可靠地找到根UI元素来获取NameScope (这里: Application.Current.MainWindow – 在设计时不起作用)


OP编辑:这个答案让我走上正轨,因为它提到了NameScope类 。

我的最终解决方案基于TestPanel1并使用INameScope接口的自定义实现。 它的每个方法都从逻辑树开始,从外部面板开始,找到NameScope属性不为null的最近的父元素:

  • RegisterNameUnregisterName将它们的调用转发到找到的INameScope对象的相应方法,否则抛出exception。
  • FindName将其调用转发到找到的INameScope对象的FindName ,否则(如果没有找到此类对象)返回null

INameScope实现的一个实例被设置为内部面板的NameScope

我不确定你是否可以完全掩盖你面板的内部结构 – 因为我知道WPF在构建视觉/逻辑树时没有使用“后门”访问,所以一旦你向用户隐藏了某些内容,你也会隐藏它来自WPF。 我要使用的是使结构“只读”(通过保持结构可访问,您不必担心绑定机制)。 为此,我建议从UIElementCollection派生并覆盖用于更改集合状态的所有方法,并将其用作面板的子集合。 至于“欺骗”XAML将子项直接添加到内部面板中,您可以简单地将ContentPropertyAttribute与暴露内部面板的ContentPropertyAttribute的属性一起使用。 这是一个工作(至少对你的测试用例)这样一个面板的例子:

 [ContentProperty("Items")] public class CustomPanel : Panel { public CustomPanel() { //the Children property seems to be lazy-loaded so we need to //call the getter to invoke CreateUIElementCollection Children.ToString(); } private readonly Panel InnerPanel = new DockPanel(); public UIElementCollection Items { get { return InnerPanel.Children; } } protected override Size ArrangeOverride(Size finalSize) { InnerPanel.Arrange(new Rect(new Point(0, 0), finalSize)); return finalSize; } protected override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent) { return new ChildCollection(this); } protected override Size MeasureOverride(Size availableSize) { InnerPanel.Measure(availableSize); return InnerPanel.DesiredSize; } private sealed class ChildCollection : UIElementCollection { public ChildCollection(CustomPanel owner) : base(owner, owner) { //call the base method (not the override) to add the inner panel base.Add(owner.InnerPanel); } public override int Add(UIElement element) { throw new NotSupportedException(); } public override void Clear() { throw new NotSupportedException(); } public override void Insert(int index, UIElement element) { throw new NotSupportedException(); } public override void Remove(UIElement element) { throw new NotSupportedException(); } public override void RemoveAt(int index) { throw new NotSupportedException(); } public override void RemoveRange(int index, int count) { throw new NotSupportedException(); } public override UIElement this[int index] { get { return base[index]; } set { throw new NotSupportedException(); } } } } 

或者,您可以跳过ContentPropertyAttribute并使用public new UIElementCollection Children { get { return InnerPanel.Children; } } – 这也可以,因为ContentPropertyAttribute("Children")是从Panelinheritance的。

备注

为了防止使用隐式样式篡改内部面板,您可能希望使用new DockPanel { Style = null }初始化内部面板。