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
property由Panel
引入,因此不存在于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
property的answer中,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 property到RelativeSource
。
除非NameScope
attached property产生有用的响应,否则有更好的方法来检索和/或绑定到当前与任何给定框架元素相关的null
?
3)使用内部面板,其子列表与外部面板的实例完全相同
不是将子列表保留在内部面板中并将调用转发到外部面板的子列表,而是反过来。这里,只使用外部面板的子列表,而内部面板从不创建自己的一个,但只使用相同的实例:
NameScope
这里,按名称布局和绑定到控件有效。但是,控件不可单击。
我怀疑我必须以某种方式将对this other question about how to react to updates in the surrounding visual tree和HitTestCore(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
派生的自定义面板会公开Grid
和ColumnDefinitions
属性,通过这些属性可以通过自定义面板的公共界面完全销毁任何自动生成的布局。作为观察所述问题的测试用例,在XAML中添加要测试的自定义面板的实例,并在该元素中添加以下内容:
DockPanel
文本块应留在文本框中,并且应显示当前在文本框中写入的内容。
我希望文本框是可点击的,输出视图不会显示任何绑定错误(因此,绑定也应该有效)。
因此,我的问题是:
答案 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
- 赢得了设计时间)
NameScope
class。
我的最终解决方案基于TestPanel1
,并使用INameScope
interface的自定义实现。它的每个方法都从逻辑树开始,从外部面板开始,找到NameScope
property不是null
的最近的父元素:
RegisterName
和UnregisterName
将其调用转发到找到的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 }
初始化内部面板。