tl;dr:我想重用预定义的现有布局逻辑WPF面板 https://msdn.microsoft.com/en-us/library/system.windows.controls.panel%28v=vs.110%29.aspx用于自定义 WPF 面板类。这个问题包含四种不同的尝试来解决这个问题,每种尝试都有不同的缺点,因此有不同的失败点。此外,还可以在下面找到一个小测试用例。
问题是:我该如何正确实现这个目标
- 定义我的自定义面板
- 内部重用另一个面板的布局逻辑,无需
- 遇到我试图解决这个问题时描述的问题吗?
我正在尝试编写一个自定义 WPFpanel https://msdn.microsoft.com/en-us/library/system.windows.controls.panel%28v=vs.110%29.aspx。对于这个面板类,我想坚持推荐的开发实践并保持干净的 API 和内部实现。具体来说,这意味着:
- 我想避免复制和粘贴代码;如果几部分代码具有相同的功能,则该代码应该只存在一次并可以重用。
- 我想应用适当的封装,并让外部用户仅访问可以安全使用的成员(不破坏任何内部逻辑,或不泄露任何内部特定于实现的信息)。
就目前而言,我将严格坚持现有的布局,我想重新使用另一个面板的布局代码(而不是像建议的那样再次编写布局代码,例如here https://stackoverflow.com/a/3223327/1430156)。为了举例,我将根据DockPanel https://msdn.microsoft.com/en-us/library/system.windows.controls.dockpanel(v=vs.110).aspx,尽管我想知道基于任何类型的一般如何做到这一点Panel
.
为了重用布局逻辑,我打算添加一个DockPanel
作为我的面板中的视觉子项,它将保存并布局我的面板的逻辑子项。
我已经尝试了三种不同的想法来解决这个问题,评论中提出了另一种想法,但到目前为止,每种想法都在不同的点上失败了:
1)在自定义面板的控件模板中引入内部布局面板
这似乎是最优雅的解决方案 - 这样,自定义面板的控制面板可以具有ItemsControl https://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol%28v=vs.110%29.aspx whose ItemsPanel财产 https://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.itemspanel(v=vs.110).aspx是使用一个DockPanel
,以及谁的ItemsSource财产 https://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.itemssource(v=vs.110).aspx绑定到Children财产 https://msdn.microsoft.com/en-us/library/system.windows.controls.panel.children%28v=vs.110%29.aspx的自定义面板。
很遗憾,Panel https://msdn.microsoft.com/en-us/library/system.windows.controls.panel%28v=vs.110%29.aspx不继承自Control https://msdn.microsoft.com/en-us/library/system.windows.controls.control%28v=vs.110%29.aspx因此没有Template财产 https://msdn.microsoft.com/en-us/library/system.windows.controls.control.template%28v=vs.110%29.aspx,也不支持控件模板。
另一方面,Children财产 https://msdn.microsoft.com/en-us/library/system.windows.controls.panel.children%28v=vs.110%29.aspx是由Panel
,因此不存在于Control
,我觉得打破预期的继承层次结构并创建一个实际上是一个面板的面板可能被认为是黑客行为Control
,但不是一个Panel
.
2)提供我的面板的子列表,它只是内部面板的子列表的包装
这样的类如下所示。我已经子类化了UIElementCollection https://msdn.microsoft.com/en-us/library/system.windows.controls.uielementcollection%28v=vs.110%29.aspx在我的面板类中,并从重写版本返回它CreateUIElementCollection method https://msdn.microsoft.com/en-us/library/system.windows.controls.panel.createuielementcollection(v=vs.110).aspx。 (我只复制了这里实际调用的方法;我已经实现了其他方法来抛出NotImplementedException https://msdn.microsoft.com/en-us/library/system.notimplementedexception%28v=vs.110%29.aspx,所以我确信没有调用其他可重写的成员。)
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;
}
}
}
这有效almost正确;这DockPanel
布局按预期被重用。唯一的问题是绑定无法按名称在面板中找到控件(使用ElementName财产 https://msdn.microsoft.com/en-us/library/system.windows.data.binding.elementname%28v=vs.110%29.aspx).
我尝试过从LogicalChildren财产 https://msdn.microsoft.com/en-us/library/system.windows.controls.panel.logicalchildren%28v=vs.110%29.aspx,但这并没有改变任何东西:
protected override System.Collections.IEnumerator LogicalChildren {
get {
return innerPanel.Children.GetEnumerator();
}
}
In an answer https://stackoverflow.com/a/31155457/1430156按用户Arie https://stackoverflow.com/users/891715/arie, the NameScope class https://msdn.microsoft.com/en-us/library/system.windows.namescope%28v=vs.100%29.aspx有人指出其中起着至关重要的作用:子控件的名称没有在相关的NameScope
因为某些原因。这might通过调用部分修复RegisterName https://msdn.microsoft.com/en-us/library/system.windows.namescope.registername(v=vs.100).aspx对于每个孩子,但需要检索正确的NameScope
实例。另外,我不确定孩子的名字发生变化时的行为是否与其他面板中的行为相同。
相反,设置NameScope
内面板的似乎是要走的路。我尝试使用简单的绑定(在TestPanel1
构造函数):
BindingOperations.SetBinding(innerPanel,
NameScope.NameScopeProperty,
new Binding("(NameScope.NameScope)") {
Source = this
});
不幸的是,这只是设置NameScope
内面板的null
。据我所知,通过Snoop https://snoopwpf.codeplex.com/, 实际上NameScope
实例仅存储在NameScope附属财产 https://msdn.microsoft.com/en-us/library/ms601183%28v=vs.100%29.aspx父窗口或由控件模板(或可能由某些其他关键节点?)定义的封闭可视化树的根,无论什么类型。当然,一个控件实例在其生命周期内可能会在控件树的不同位置添加和删除,因此相关的NameScope
可能会不时发生变化。这再次需要绑定。
这就是我再次陷入困境的地方,因为不幸的是,人们无法定义一个RelativeSource https://msdn.microsoft.com/en-us/library/system.windows.data.relativesource%28v=vs.100%29.aspx用于基于任意条件的绑定,例如*第一个遇到的具有非null
分配给的值NameScope附属财产 https://msdn.microsoft.com/en-us/library/ms601183%28v=vs.100%29.aspx.
Unless 关于如何对周围视觉树中的更新做出反应的另一个问题 https://stackoverflow.com/questions/31187522/capture-change-in-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) https://msdn.microsoft.com/en-us/library/ms598914%28v=vs.110%29.aspx and to HitTestCore(PointHitTestParameters) https://msdn.microsoft.com/en-us/library/ms598915%28v=vs.110%29.aspx到内面板。但是,在内部面板中,我只能访问InputHitTest https://msdn.microsoft.com/en-us/library/system.windows.uielement.inputhittest%28v=vs.110%29.aspx,所以我不确定如何安全地处理原始数据HitTestResult https://msdn.microsoft.com/en-us/library/system.windows.media.hittestresult%28v=vs.110%29.aspx实例不会丢失或忽略原始实现会尊重的任何信息,也不知道如何处理GeometryHitTestParameters https://msdn.microsoft.com/en-us/library/system.windows.media.geometryhittestparameters%28v=vs.110%29.aspx, as InputHitTest
只接受简单的Point https://msdn.microsoft.com/en-us/library/system.windows.point(v=vs.110).aspx.
Moreover, the controls are also not focusable, e.g. by pressing Tab. I do not know how to fix this.
此外,我对这种方式有点谨慎,因为我不确定通过用自定义对象替换该子列表来打破内部面板和原始子列表之间的内部链接。
4)直接继承面板类
User Clemens https://stackoverflow.com/users/1136211/clemens建议直接让我的类继承DockPanel https://msdn.microsoft.com/en-us/library/system.windows.controls.dockpanel%28v=vs.110%29.aspx。然而,有两个原因表明这不是一个好主意:
- 我的面板的当前版本将依赖于布局逻辑
DockPanel
。然而,有可能在未来的某个时候,这还不够,有人确实必须在我的面板中编写自定义布局逻辑。在这种情况下,更换内部DockPanel
使用自定义布局代码很简单,但是删除DockPanel
从我的面板的继承层次结构来看,这意味着一个重大的改变。
- 如果我的面板继承自DockPanel https://msdn.microsoft.com/en-us/library/system.windows.controls.dockpanel%28v=vs.110%29.aspx,面板的用户可能会通过乱搞暴露的属性来破坏其布局代码
DockPanel
, 尤其LastChildFill https://msdn.microsoft.com/en-us/library/system.windows.controls.dockpanel.lastchildfill%28v=vs.110%29.aspx。虽然这只是该属性,但我想使用一种适用于所有属性的方法Panel https://msdn.microsoft.com/en-us/library/system.windows.controls.panel%28v=vs.110%29.aspx亚型。例如,派生自的自定义面板Grid https://msdn.microsoft.com/en-us/library/system.windows.controls.grid%28v=vs.110%29.aspx会暴露ColumnDefinitions https://msdn.microsoft.com/en-us/library/system.windows.controls.grid.columndefinitions%28v=vs.110%29.aspx and RowDefinitions https://msdn.microsoft.com/en-us/library/system.windows.controls.grid.rowdefinitions%28v=vs.110%29.aspx属性,通过这些属性,任何自动生成的布局都可以通过自定义面板的公共接口被完全销毁。
作为观察所描述问题的测试用例,添加在 XAML 中测试的自定义面板的实例,并在该元素中添加以下内容:
<TextBox Name="tb1" DockPanel.Dock="Right"/>
<TextBlock Text="{Binding Text, ElementName=tb1}" DockPanel.Dock="Left"/>
文本块应位于文本框的左侧,并且应显示文本框中当前写入的内容。
我希望文本框是可单击的,并且输出视图不显示任何绑定错误(因此,绑定也应该起作用)。
因此,我的问题是:
- 我的任何一项尝试是否可以修复以得出完全正确的解决方案?或者是否有一种完全其他的方式比我尝试做的事情更好?