如何为新的面板类重用现有的布局代码?

Posted

技术标签:

【中文标题】如何为新的面板类重用现有的布局代码?【英文标题】:How to Reuse Existing Layouting Code for new Panel Class? 【发布时间】:2015-08-22 01:24:39 【问题描述】:

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

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

同时定义我的自定义面板 内部重用另一个面板的布局逻辑,没有 遇到了我在尝试解决此问题时描述的问题?

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

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

就目前而言,我将密切关注现有布局,我想重新使用另一个面板的布局代码(而不是像建议的那样再次编写布局代码,例如here)。举个例子,我将基于DockPanel进行解释,尽管我想知道如何根据任何类型的Panel进行一般性操作。

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

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


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

这似乎是最优雅的解决方案 - 这样,自定义面板的控制面板可以具有 ItemsControl,其 ItemsPanel property 使用 DockPanel,其 ItemsSource property 绑定到 Children property自定义面板。

很遗憾,Panel 不继承自 Control,因此不具有 Template property,也不支持控件模板。

另一方面,Children property 是由Panel 引入的,因此在Control 中不存在,我觉得打破预期的继承层次并创建一个面板可能被认为是 hacky实际上是Control,但不是Panel


2) 提供我的面板的子列表,它只是内部面板的子列表的包装

这样的类如下图所示。我在面板类中对UIElementCollection 进行了子类化,并从CreateUIElementCollection method 的覆盖版本返回它。 (我只复制了这里实际调用的方法;我已经实现了其他的抛出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 property)找到控件。

我已经尝试从LogicalChildren property 返回内部子级,但这并没有改变任何东西:

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

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

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

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

不幸的是,这只是将内部面板的NameScope 设置为null。据我通过Snoop 发现,实际的NameScope 实例仅存储在父窗口的NameScope attached property 中,或者由控件模板定义的封闭可视化树的根目录中(或者可能由其他关键节点?),无论是什么类型。当然,一个控件实例在其生命周期内可能会在控件树的不同位置添加和删除,因此相关的NameScope可能会不时发生变化。这又需要一个绑定。

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

除非this other question about how to react to updates in the surrounding visual tree 产生有用的响应,否则是否有更好的方法来检索和/或绑定到当前与任何给定框架元素相关的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(GeometryHitTestParameters)HitTestCore(PointHitTestParameters) 到内部面板。但是,在内部面板中,我只能访问InputHitTest,因此我不确定如何安全地处理原始HitTestResult 实例而不丢失或忽略任何原始实现会尊重的信息,也不知道如何处理GeometryHitTestParameters,因为InputHitTest 只接受一个简单的Point

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

此外,我对采用这种方式有点谨慎,因为我不确定内部面板和原始子列表之间的内部链接是什么,我通过将子列表替换为自定义对象来破坏该列表。


4) 直接从面板类继承

用户Clemens 建议直接让我的类继承自DockPanel。但是,这不是一个好主意有两个原因:

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

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

<TextBox Name="tb1" DockPanel.Dock="Right"/>
<TextBlock Text="Binding Text, ElementName=tb1" DockPanel.Dock="Left"/>

文本块应该在文本框的左边,它应该显示当前在文本框中写入的内容。

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


因此,我的问题是:

我的任何一个尝试都可以修复以得到完全正确的解决方案吗?或者有没有比我尝试做我正在寻找的事情更可取的完全其他方式?

【问题讨论】:

有人能解释一下否决票吗? 自定义面板的布局与 DockPanel 的布局有何不同?或许可以从DockPanel派生出来,调用基类的方法后实现MeasureOverride和ArrangeOverride的区别。 @Clemens:我想用与DockPanel 不同的附加属性来控制布局。在内部,当设置我自己的附加属性之一时,应该设置 DockPanel 属性。但是,我打算将其作为实现细节;例如,很有可能在未来的版本中,内部的DockPanel 将被替换为Grid(如果发生这种情况,我仍然不希望ColumnDefinitionsRowDefinitions 在外部可见) . @Clemens:无论如何,谢谢你的评论,我已将建议添加到问题中。 只需将 DockPanel 布局代码的必要部分复制到您的 Panel 类中? 【参考方案1】:

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

    public DependencyObject this[string childName]
    
        get
        
            return innerPanel.FindChild<DependencyObject>(childName);
        
    

然后,示例绑定:

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

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


编辑:

如果您希望“常规”binding by ElementName 工作,则必须在适当的NameScope 中使用作为 innerPanel 子级的 register the names of controls:

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 class。

我的最终解决方案基于TestPanel1,并使用INameScope interface 的自定义实现。它的每个方法都从外部面板开始沿着逻辑树向上走,以找到最近的父元素,其NameScope property 不是null

RegisterNameUnregisterName 将它们的调用转发给找到 INameScope 对象的各自方法,否则抛出异常。 FindName 将其调用转发到找到的 INameScope 对象的 FindName,否则(如果未找到此类对象)返回 null

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

【讨论】:

但是控件的用户必须知道由于某种原因,绑定到子控件的通常方式(通过Binding ElementName=innerPanelButtonName, Path=Content)不起作用并且具有使用此答案中建议的索引符号来解决,这看起来像一个错误。毕竟,没有其他 WPF 面板需要这样的语法,它们都直接使用 ElementName 在这种情况下可以启用“正常”的 ElementName 绑定。您必须在根 UI 元素的 NameScope 中的您的 innerPanel 中注册控件的名称。现在的问题是如何可靠地找到它(例如 Application.Current.MainWindow 将在设计时返回 null )。我正在更新我的答案... 编辑看起来很有希望。我想它引发了一个问题,为什么内部面板的子级与外部面板的子级不在同一个名称范围内,或者我们是否以及如何为两者强制执行相同的名称范围(否则,我认为事情会变得丑陋当名称在运行时更改时,我假设名称范围的内置默认处理会处理)。面板本身(甚至是外部面板)似乎没有设置任何名称范围;但是,Snoop 不仅告诉我主机 Window,而且直接嵌套在该窗口中的 Border 具有非空名称范围。 我已经扩展了我的方法 2) 的描述,以包含从您的答案中检索到的有关 NameScope 的有价值信息。 另外,我已经启动了a separate question,它可能会为如何检索和更新适当的NameScope 提供一些线索。【参考方案2】:

我不确定您是否可以完全掩盖面板的内部结构 - 据我所知,WPF 在构建视觉/逻辑树时没有使用“后门”访问,因此一旦您向用户隐藏某些内容,您也将其隐藏在 WPF 中。我要做的是使结构“只读”(通过保持结构可访问,您无需担心绑定机制)。为此,我建议从UIElementCollection 派生并覆盖所有用于更改集合状态的方法,并将其用作面板的子集合。至于“欺骗”XAML 将子项直接添加到内部面板中,您可以简单地使用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") 继承自 Panel

备注

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

【讨论】:

以上是关于如何为新的面板类重用现有的布局代码?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 Ifc2x3 和 Ifc4 重用相同的 xBim 代码

如何为 dojox.mobile 视图编写可重用的控制器代码

如何为具有spring存储库代码的@Component类编写集成测试?

如何为 iPhone Light 版本应用程序创建 xcode 项目?

如何为现有的 WCF 服务原生启用 JSONP?

第七篇7.1章