tl;dr: I want to reuse the existing layouting logic of a pre-defined WPF panel for a custom WPF panel class. This question contains four different attempts to solve this, each with different downsides and thus a different point of failure. Also, a small test case can be found further down.
The question is: How do I properly achieve this goal of
- defining my custom panel while
- internally reusing the layouting logic of another panel, without
- running into the problems described in my attempts to solve this?
I am trying to write a custom WPF panel. For this panel class, I would like to stick to recommended development practices and maintain a clean API and internal implementation. Concretely, that means:
- I would like to avoid copy and pasting of code; if several portions of code have the same function, the code should exist only once and be reused.
- I would like to apply proper encapsulation and let outside users access only such members that can safely be used (without breaking any of the internal logic, or without giving away any internal implementation-specific information).
As for the time being, I am going to closely stick with an existing layout, I would like to re-use another panel's layouting code (rather than writing the layouting code again, as suggested e.g. here). For the sake of an example, I will explain based on DockPanel
, though I would like to know how to do this generally, based on any kind of Panel
.
To reuse the layouting logic, I am intending to add a DockPanel
as a visual child in my panel, which will then hold and layout the logical children of my panel.
I have tried three different ideas on how to solve this, and another one was suggested in a comment, but each of them so far fails at a different point:
1) Introduce the inner layout panel in a control template for the custom panel
This seems like the most elegant solution - this way, a control panel for the custom panel could feature an ItemsControl
whose ItemsPanel
property is uses a DockPanel
, and whose ItemsSource
property is bound to the Children
property of the custom panel.
Unfortunately, Panel
does not inherit from Control
and hence does not have a Template
property, nor feature support for control templates.
On the other hand, the Children
property is introduced by Panel
, and hence not present in Control
, and I feel it could be considered hacky to break out of the intended inheritance hierarchy and create a panel that is actually a Control
, but not a Panel
.
2) Provide a children list of my panel that is merely a wrapper around the children list of the inner panel
Such a class looks as depicted below. I have subclassed UIElementCollection
in my panel class and returned it from an overridden version of the CreateUIElementCollection
method. (I have only copied the methods that are actually invoked here; I have implemented the others to throw a NotImplementedException
, so I am certain that no other overrideable members were invoked.)
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;
}
}
}
This works almost correctly; the DockPanel
layout is reused as expected. The only issue is that bindings do not find controls in the panel by name (with the ElementName
property).
I have tried returned the inner children from the LogicalChildren
property, but this did not change anything:
protected override System.Collections.IEnumerator LogicalChildren {
get {
return innerPanel.Children.GetEnumerator();
}
}
In an answer by user Arie, the NameScope
class was pointed out to have a crucial role in this: The names of the child controls do not get registered in the relevant NameScope
for some reason. This might be partially fixed by invoking RegisterName
for each child, but one would need to retrieve the correct NameScope
instance. Also, I am not sure whether the behaviour when, for instance, the name of a child changes would be the same as in other panels.
Instead, setting the NameScope
of the inner panel seems to be the way to go. I tried this with a straightforward binding (in the TestPanel1
constructor):
BindingOperations.SetBinding(innerPanel,
NameScope.NameScopeProperty,
new Binding("(NameScope.NameScope)") {
Source = this
});
Unfortunately, this just sets the NameScope
of the inner panel to null
. As far as I could find out by means of Snoop, the actual NameScope
instance is only stored in the NameScope
attached property of either the parent window, or the root of the enclosing visual tree defined by a control template (or possibly by some other key node?), no matter what type. Of course, a control instance may be added and removed at different positions in a control tree during its lifetime, so the relevant NameScope
might change from time to time. This, again, calls for a binding.
This is where I am stuck again, because unfortunately, one cannot define a RelativeSource
for the binding based on an arbitrary condition such as *the first encountered node that has a non-null
value assigned to the NameScope
attached property.
Unless this other question about how to react to updates in the surrounding visual tree yields a useful response, is there a better way to retrieve and/or bind to the NameScope
currently relevant for any given framework element?
3) Use an inner panel whose children list is simply the same instance as that of the outer panel
Rather than keeping the child list in the inner panel and forwarding calls to the outer panel's child list, this works kind-of the other way round. Here, only the outer panel's child list is used, while the inner panel never creates one of its own, but simply uses the same instance:
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;
}
}
}
Here, layouting and binding to controls by name works. However, the controls are not clickable.
I suspect I have to somehow forward calls to HitTestCore(GeometryHitTestParameters)
and to HitTestCore(PointHitTestParameters)
to the inner panel. However, in the inner panel, I can only access InputHitTest
, so I am neither sure how to safely process the raw HitTestResult
instance without losing or ignoring any of the information that the original implementation would have respected, nor how to process the GeometryHitTestParameters
, as InputHitTest
only accepts a simple Point
.
Moreover, the controls are also not focusable, e.g. by pressing Tab. I do not know how to fix this.
Besides, I am slightly wary of going this way, as I am not sure what internal links between the inner panel and the original list of children I am breaking by replacing that list of children with a custom object.
4) Inherit directly from panel class
User Clemens suggests to directly have my class inherit from DockPanel
. However, there are two reasons why that is not a good idea:
- The current version of my panel will rely on the layouting logic of
DockPanel
. However, it is possible that at some point in the future, that will not be enough any more and someone will indeed have to write custom layouting logic in my panel. In that case, replacing an innerDockPanel
with custom layouting code is trivial, but removingDockPanel
from the inheritance hierarchy of my panel would mean a breaking change. - If my panel inherits from
DockPanel
, users of the panel might be able to sabotage its layouting code by messing around with properties exposed byDockPanel
, in particularLastChildFill
. And while it is just that property, I would like to use an approach that works with allPanel
subtypes. For instance, a custom panel derived fromGrid
would expose theColumnDefinitions
andRowDefinitions
properties, by which any automatically generated layout could be completely destroyed via the public interface of the custom panel.
As a test case for observing the described issues, add an instance of the custom panel being tested in XAML, and within that element, add the following:
<TextBox Name="tb1" DockPanel.Dock="Right"/>
<TextBlock Text="{Binding Text, ElementName=tb1}" DockPanel.Dock="Left"/>
The text block should be left of the text box, and it should show whatever is currently written in the text box.
I would expect the text box to be clickable, and the output view not to display any binding errors (so, the binding should work, as well).
Thus, my question is:
- Can any one of my attempts be fixed to lead to a completely correct solution? Or is there a completely other way that is preferrable to what I have tried to do what I am looking for?
DockPanel
. Internally, when setting one of my own attached properties, theDockPanel
properties should be set. However, I intend for that to be an implementation detail; it is well possible that, for instance, the innerDockPanel
will be replaced with aGrid
in future versions (and if that happens, I still don't want theColumnDefinitions
andRowDefinitions
to be visible externally). – InfatuatedDockPanel
is still fairly simple, but replicating the layouting logic of aGrid
sounds like a major task. – InfatuatedItems
instead ofChildren
), or some hacky hybrid class like approach 2. – Candlelight