如何为新的Panel类重用现有的Layouting代码?

时间:2015-06-07 14:18:32

标签: c# .net wpf panel layoutpanels

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

问题是:如何正确实现

的目标
  • 时定义我的自定义面板
  • 在内部重用其他面板的布局逻辑,不带
  • 遇到我试图解决这个问题所描述的问题?

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

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

就目前而言,我将密切关注现有的布局,我想重新使用另一个面板的布局代码(而不是再次编写布局代码,如建议的那样{{3 }})。为了举例,我将基于here进行解释,但我想基于任何类型Panel来了解如何做到这一点。

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

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

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

这似乎是最优雅的解决方案 - 这样,自定义面板的控制面板可能会显示DockPanel,其ItemsControl使用DockPanel,其ItemsPanel property }绑定到自定义面板的ItemsSource property

不幸的是,Children property不会从Panel继承,因此没有Control,也没有对控件模板的功能支持。

另一方面,Template propertyPanel引入,因此不存在于Control中,我觉得打破预期的继承层次结构可能会被视为hacky并创建一个实际为Control的面板,但不是Panel

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

这样的课程如下所示。我在我的面板类中已经将Children property子类化,并从UIElementCollection的重写版本返回。 (我只复制了这里实际调用的方法;我已经实现了其他方法来抛出CreateUIElementCollection method,所以我确信没有调用其他可覆盖的成员。)

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布局按预期重复使用。唯一的问题是绑定没有按名称在面板中找到控件(使用NotImplementedException)。

我试过从ElementName property返回内心的孩子,但这并没有改变任何东西:

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

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

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

TestPanel1

不幸的是,这只是将内部面板的 BindingOperations.SetBinding(innerPanel, NameScope.NameScopeProperty, new Binding("(NameScope.NameScope)") { Source = this }); 设置为NameScope。据我所知,通过RegisterName,实际的null实例只存储在父窗口的Snoop中,或者由...定义的封闭可视树的根。无论什么类型,控制模板(或可能由其他一些关键节点?)。当然,可以在控制树的生命周期中在控制树中的不同位置添加和删除控件实例,因此相关的NameScope可能会不时发生变化。这再次要求绑定。

这是我再次陷入困境的地方,因为遗憾的是,根据任意条件(例如*分配了非NameScope值的第一个遇到的节点,无法为绑定定义NameScope attached propertyRelativeSource

除非NameScope attached property产生有用的响应,否则有更好的方法来检索和/或绑定到当前与任何给定框架元素相关的null

3)使用内部面板,其子列表与外部面板的实例完全相同

不是将子列表保留在内部面板中并将调用转发到外部面板的子列表,而是反过来。这里,只使用外部面板的子列表,而内部面板从不创建自己的一个,但只使用相同的实例:

NameScope

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

我怀疑我必须以某种方式将对this other question about how to react to updates in the surrounding visual treeHitTestCore(GeometryHitTestParameters)的来电转发到内部小组。但是,在内部面板中,我只能访问HitTestCore(PointHitTestParameters),所以我既不确定如何安全地处理原始InputHitTest实例而不会丢失或忽略原始实现所尊重的任何信息,以及如何处理HitTestResult,因为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; } } } 只接受一个简单的GeometryHitTestParameters

此外,控制也无法集中,例如按 Tab 。我不知道如何解决这个问题。

此外,我对这种方式略显警惕,因为我不确定内部面板与原始儿童列表之间的内部链接是否通过用自定义对象替换该子列表来打破。

4)直接从小组类继承

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

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

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

DockPanel

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

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

因此,我的问题是:

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

2 个答案:

答案 0 :(得分:2)

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

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

然后,示例绑定:

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

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

修改

如果你想要&#34;通常&#34;要binding by ElementName工作,您必须register the names of controls作为相应NameScope中内部面板子项的子项{/ 3}}:

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将其调用转发到找到的FindName对象的INameScope,否则(如果找不到此类对象)返回null

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

答案 1 :(得分:2)

我不确定你是否可以完全掩盖你面板的内部结构 - 因为我不知道所有的后门&#34;后门&#34; WPF在构建可视/逻辑树时使用访问权限,因此一旦您向用户隐藏了某些内容,您也会将其隐藏在WPF中。我所要做的就是制作结构&#34;只读&#34; (通过保持结构可访问,您不必担心绑定机制)。为此,我建议从UIElementCollection派生并覆盖用于更改集合状态的所有方法,并将其用作面板的子集合。至于&#34;欺骗&#34;将孩子直接添加到内部面板中的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

<强> REMARK

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