tl;dr:我想为自定义 WPF 面板类重用预定义WPF 面板的现有布局逻辑。这个问题包含四种不同的尝试来解决这个问题,每一种都有不同的缺点,因此也有不同的失败点。此外,可以在更下方找到一个小型测试用例。
问题是:我如何正确实现这一目标
- 在定义我的自定义面板时
- 内部重用另一个面板的布局逻辑,没有
- 遇到我试图解决这个问题时描述的问题?
我正在尝试编写自定义 WPF面板。对于这个面板类,我想坚持推荐的开发实践,并保持一个干净的 API 和内部实现。具体来说,这意味着:
- 我想避免复制和粘贴代码;如果代码的几个部分具有相同的功能,则该代码应该只存在一次并且可以重复使用。
- 我想应用适当的封装,让外部用户只访问可以安全使用的成员(不破坏任何内部逻辑,或不泄露任何内部实现特定信息)。
就目前而言,我将密切关注现有布局,我想重新使用另一个面板的布局代码(而不是再次编写布局代码,如此处所建议的那样)。举个例子,我将DockPanel
根据Panel
.
为了重用布局逻辑,我打算DockPanel
在我的面板中添加一个作为可视子项,然后它将保存和布局我的面板的逻辑子项。
我已经尝试了三种不同的想法来解决这个问题,并且在评论中提出了另一种想法,但到目前为止,它们中的每一个都在不同的点上失败了:
1)在自定义面板的控件模板中引入内部布局面板
这似乎是最优雅的解决方案 - 这样,自定义面板的控制面板可以具有ItemsControl
其ItemsPanel
属性使用 a DockPanel
,并且其ItemsSource
属性绑定到自定义面板的Children
属性。
不幸的是,Panel
它没有继承自Control
,因此没有Template
属性,也没有对控件模板的功能支持。
另一方面,该Children
属性是由 引入的Panel
,因此不存在于 中,我觉得打破预期的继承层次并创建一个实际上是 a而不是 aControl
的面板可能被认为是 hacky 。Control
Panel
2) 提供我的面板的子列表,它只是内部面板的子列表的包装
这样的类如下所示。我UIElementCollection
在面板类中进行了子类化,并从CreateUIElementCollection
方法的覆盖版本返回它。(我只复制了这里实际调用的方法;我已经实现了其他方法来抛出 a 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
根据任意条件为绑定定义 a ,例如 *the first遇到的节点具有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;
}
}
}
在这里,按名称布局和绑定到控件是可行的。但是,这些控件不可单击。
我怀疑我必须以某种方式将呼叫转发到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"/>
文本块应该位于文本框的左侧,并且应该显示当前在文本框中写入的任何内容。
我希望文本框是可点击的,并且输出视图不会显示任何绑定错误(因此,绑定也应该有效)。
因此,我的问题是:
- 我的任何一项尝试都可以解决以导致完全正确的解决方案吗?还是有一种完全其他的方式比我试图做的事情更可取?