Introduction
Have you ever wished an element could have a parent that was neither visual nor logical? Okay, let me ask that another way... Have you ever seen either of the following exceptions?
ArgumentException: Specified Visual is already a child of
another Visual or the root of a CompositionTarget.
InvalidOperationException: Specified element is already
the logical child of another element. Disconnect it first.
As these error messages indicate, in Windows Presentation Foundation (WPF), an element can have no more than one visual parent and no more than one logical parent. This is actually very important because it ensures that the visual and logical trees are well-formed.
Although I fully acknowledge the importance of this requirement, there have certainly been occasions where I really wanted one panel to merely be the "conceptual" parent of an element so that another panel could actually assume the visual and/or logical parental role.
I first encountered this issue about 5 years ago while working on a demo for Microsoft's Professional Developer's Conference (PDC) 2003. If you attended PDC '03, you might recall seeing the application shown here during the opening keynote.
I grabbed this old bootlegged image from this very informative article. Okay, so I don’t actually read Japanese, and maybe you don’t either, but if you scroll about half way down, you'll find a couple of pictures labeled WinFS+Avalon and WinFS, respectively.
Side note: If you're wondering what Avalon is, it's what WPF was called when it had a cool name. And, if you're wondering what WinFS is, don't worry about it. It seems to be a dead file system technology. I wrote the WinFS portion of this demo application, and I actually liked it. This demo was the first public revealing of Avalon, and it may well represent the last public showing of WinFS. (I choose to believe that the demise of WinFS had nothing to do with my demo code!)
Back to the point... I’m hopeful that, in addition to talking about WinFS, the article is describing how this application showed off Avalon's powerful animation support by demonstrating layout-to-layout animation of visual elements.
As you may know, the WPF platform does not natively support layout-to-layout animation. Why? Well, because a child can only have a single visual parent. That is to say, a visual child can only belong to a single layout panel. So, to pull off this effect in WPF, you are not really animating a child from one layout panel into another layout panel. You are faking it.
Another side note: For those who are curious, the illusion of layout-to-layout animation in this application was invoked by a method called SendInTheClones()
. In this routine, we cloned all the elements in one layout panel, added them to the second layout panel, applied transforms to get them to line up with the elements in the first panel, faded out the actual elements, and then animated the clones to their natural positions in the second panel. Uuggh! This painful technique is still often used in WPF. (I am happy to see support for true layout-to-layout animation in a preview release of a new blendables Layout mix for WPF. In addition to 2D layout, the blendables team is also making 3D layout more accessible in a CTP release of a 3D mix.)
Why bring this up now?
Recently, one of my fellow WPF disciples, Sacha Barber, expressed a desire to track the visual children of a custom panel so that he could build a 3D scene representing those children. Since a panel exists to provide layout for its children, it makes perfect sense that you might want to have a panel position elements in 3D space, rather than 2D space.
Unfortunately, this is not so easily done in WPF. There are completely separate layout engines for 2D visuals and 3D visuals. It's true that some great work has been done to enable the mapping of 2D visuals into 3D space in a way that allows those visuals to be interactive (via the Viewport2DVisual3D
element), but this is built on top of the 3D layout engine. Sacha's goal was to somehow hook the 2D layout engine to obtain the visuals and then position those visuals using the 3D layout engine.
He naturally arrived at the idea that he could override OnVisualChildrenChanged()
in his panel and then use each new child as the visual for a VisualBrush
that could be used in the 3D scene. He brought his idea to the group for advice on a couple of implementation details.
While working with Sacha on this concept, another fellow disciple, Josh Smith, immediately saw the potential of such a panel. He came up with his own clever idea of using the adorner layer to host a Viewport3D
that is overlaid on top of the panel. This would allow the children to be measured and arranged in the 2D panel, and then each child could be presented in the 3D scene, again using a VisualBrush
. Being the preeminent WPF rock star that he is, Josh quickly put together a prototype and, with Sacha’s permission, blogged about it.
Clones in Space
This was a very cool implementation, but a visual brush representation of an element is not the same as having an interactive 2D element in 3D space. Josh set out to conquer this problem next. He effectively cloned the elements by creating a ContentPresenter
with the same data context and data template as the child within the panel. This new ContentPresenter
was then used as the visual for a Viewport2DVisual3D
element that was added to the 3D scene in the adorner layer.
You can check out Josh's fully interactive sample here. Brilliant!
After seeing this clever use of a 2D panel presenting a view of its children in 3D space, I did a quick search to see if anyone else had done something similar. I found that Pavan Podila took almost the exact same approach when he promoted his ElementFlow concept to a panel. Clearly, great minds think alike!
The Remaining Problems
Watching Sacha and Josh go back and forth on this concept and then seeing a couple examples of it in action really got my mind racing! The adorner layer approach with clones in space is just plain cool, as is the direct visual child approach. Still, a couple of things about the scenario really bothered me (and Josh and Sacha too, quite frankly).
- Why should the panel need to manage duplicate representations of its children?
With a complex item template, this approach might involve a lot of extra visuals! It would be better to just use the actual panel's children in the 3D scene so that there is only one visual instance of each child. Oh yeah, those elements are already visual children of the panel. Damn!
- Why should the items within the
Children
collection even be visual children of the panel?
These children are certainly not visual in concept. Conceptually, they are just children... not logical... not visual... just conceptual.
What do I mean by, "they are not visual in concept"? Simply that we do not want the 2D layout engine to view these visuals as children of the panel because that means it will try to enumerate and render these children when rendering the panel.
What do I mean by, "they are conceptual children"? Simply that we do still want the elements to be children, in concept. That is, we want the elements to be directly added to the Children
collection of the panel. This gives us the critical ability to use the panel wherever a 2D panel is traditionally used.
Both of these problems stem from the fact that a panel's children are maintained within a special collection of UIElement
objects that is appropriately named UIElementCollection
. This collection automatically makes its members visual children of the panel (and often, it also makes them logical children). UIElementCollection
also enforces a rule that only the item container generator can modify the panel's Children
collection while the panel is an items host.
Side note: If you are curious, the ItemContainerGenerator
class is a special framework class that is in charge of ensuring that each item within an ItemsControl
is "contained" within a special UIElement
specific to that type of ItemsControl
. See: 'I' is for Item Container, for more information.
An Introduction to "Conceptual Children"
You can't choose your parents, so you might as well choose your children!
Clearly, what we need is a panel class that does not live by the rules of UIElementCollection
. We still want the panel to work like every other panel. We should be able to add elements to the panel and they should automatically go into its Children
collection. But those children should not automatically become visual children or even logical children of the panel. Ideally, they should just represent a collection of disconnected visuals, as far as the framework is concerned. The term I have coined for this new relationship is "conceptual children".
Obviously, we should still be able to use the panel as an items host. This means that its Children
collection must work seamlessly with an item container generator, just as UIElementCollection
does for native panels.
ConceptualPanel, LogicalPanel, and DisconnectedUIElementCollection
The rest of this article contains the details regarding my implementation of a couple of new panels called ConceptualPanel
and LogicalPanel
. The first of these panels supports this new notion of a purely conceptual child collection. The second panel derives from ConceptualPanel
, and promotes its children to logical children. Both panels leverage a special new collection called DisconnectedUIElementCollection
.
If you care about the details as to how all this works, keep reading. If you merely want to see a panel in action, you can now jump to the Seeing is Believing section at the end of this article.
The Dirty Details
For reasons which should become obvious as we proceed, I feel that it is not only necessary to explain why I have created these new panels, but to also explain how they are implemented. You are certainly welcome to look at the code, but I also wanted a written explanation just to be completely clear as to why I made the choices I did. Just about every line of code was implemented for a specific reason. Hopefully, the relevant explanations can all be found here. If not, please let me know what is missing.
There is always room for improvement. As per usual, I am releasing this code under a BSD open source license and putting it into the public domain so that it can be used, stressed, and improved. If you make noteworthy enhancements to these classes, please send the updated code my way so that I can make it widely available. As always, I'm open to suggestions, questions, feedback, and other comments. There is a comments area below, but if you require more than a couple of lines, please drop me an email. I will strive to incorporate all such feedback back into this article.
Caution: The beverage you are about to enjoy is extremely hot!
What follows, can only be described as a first rate hack attack on UIElementCollection
! If you are not comfortable with the notion of hacking the framework, you should stop reading now and return to your well-defined box. The following territory is only for those who wish to venture outside the box. ;-)
What do I mean by "first rate hack"? Simply this. The code is written to take advantage of certain implementation details that are internal to the framework. I believe it to be a very solid solution for code that runs on .NET 3.0 and 3.5. At the same time, I fully recognize that Microsoft has full authority to change the internal workings of its platform in whatever way it deems necessary. It's possible that Microsoft may change the framework in a manner that would preclude this solution from working in the future. I'm hoping that they will recognize the utter coolness that is enabled by having a true Panel
class that treats its children in a purely conceptual manner. If they decide to remove the ability to achieve this notion via the method described herein, hopefully they will enable it natively. :-)
There's the disclaimer. Now, let's begin our attack!
A Brief Look at UIElementCollection
To really understand how the children of a typical panel are maintained, we must first understand how UIElementCollection
works. Here are the salient points:
1. The Children property of a Panel is of type UIElementCollection
The Panel
class exposes a public property called Children
that is of type UIElementCollection
. It overrides its VisualChildrenCount
property and GetVisualChild()
method to return the members of this Children
collection as its visual children. It also uses the GetVisualChild()
override to provide z-ordering for its children.
2. UIElementCollection provides an extremely handy way to provide any element, not just a panel, with a collection of visual (and optionally, logical) children
An element can simply create an instance of UIElementCollection
and supply itself as the visual parent for the collection. Then, any UIElement
that is added to the collection is automatically added as a visual child of the element. If a logical parent was also specified when the collection was created, then the child is also added as a logical child of the given element.
3. UIElementCollection stores its members in a VisualCollection
Internally, a VisualCollection
instance is used to store the members of a UIElementCollection
. This instance is created within the constructor of UIElementCollection
. VisualCollection
is the actual class that sets up the visual parent/child relationship between the owning element and the child element. It must be created with a valid visual parent. UIElementCollection
then extends the functionality of VisualCollection
by adding support for the logical parent/child relationship.
4. UIElementCollection behaves differently when owned by a Panel
When it is used to host the children of an "items host", UIElementCollection
prevents direct modification of its collection. (See 'P' is for Panel for an explanation of what it means to be an items host [a.k.a., items panel].) If the panel is not an items host, then direct modification of the Children
collection is allowed.
5. UIElementCollection has a back door
There are special internal methods on UIElementCollection
that allow the framework to modify its internal visual collection when a panel is an items host. These methods are accessed primarily by the item container generator of the associated ItemsControl
.
6. UIElementCollection pretends to be extensible
The UIElementCollection
class exposes most of its members as virtual functions. Additionally, the Panel
class exposes a virtual function called CreateUIElementCollection
that can be overridden to create a custom collection that derives from and extends UIElementCollection
. The implication is that we should be able to provide a custom collection and monitor changes to it by overriding the appropriate methods: Add
, Clear
, Insert
, Remove
, RemoveAt
, etc.
7. UIElementCollection is not really extensible
Although it is true that we can derive and use our own custom UIElementCollection
class within a custom panel, I now refer you back to item 5 above... UIElementCollection
has a back door! As it turns out, our method overrides will work perfectly well as long as we only use the panel outside of an ItemsControl
. As soon as we try to use our panel as an items host, the framework will bypass all of our overrides on our custom UIElementCollection
and use its backdoor to modify our class' internal VisualCollection
. That's right! Visual children will be added directly to our panel without giving us any opportunity to insert our custom logic.
8. We are going to extend UIElementCollection anyway!
It should now be clear that the framework doesn't want us mucking with the internal VisualCollection
that is used to store the visual children of a panel. The very fact that it bypasses our custom logic under data-bound scenarios so that it can use its internal logic tells us that much. So, let's get busy and hack this puppy...
Remember our objective? We want to create a derivative of the Panel
class that does not act as either visual parent or logical parent to its children. Essentially, we want the members of our Children
collection to be unparented. They should be purely conceptual children.
Avoiding Parental Obligations is Not Easy!
I will admit that I went down many wrong paths before I ultimately arrived at a solution. My first idea was to simply watch OnVisualChildrenChanged
and unparent each child as it arrived. The problem with this approach is that the framework doesn't know you are unparenting the children of the internal VisualCollection
. By the very nature of being in that collection, those elements are assumed to be visual children of their owner. When such an element is removed from the collection, the framework will observe the broken relationship and throw an exception. Since this very often happens through the back door, you cannot insert custom logic to circumvent the exception.
Many of my other attempts at avoiding parental responsibilities would eventually become pieces of the ultimate working solution, so I won't bore you with anymore of the roadblocks I encountered along the way. Let's just look at the architecture of the working solution.
Going forward, I'm going to build this solution as if it’s an exercise we're working on together. We can pretend that we are perfect coders and know everything we need to about the internal workings of the framework (although the real progression was not nearly so elegant and required deep reflection... literally).
Introducing the DisconnectedUIElementCollection Class
By now, I think it's evident that any solution we build needs to be based on a custom UIElementCollection
. We begin by defining a class called DisconnectedUIElementCollection
(so named because its members will not automatically be connected to the owner of the collection via logical or visual relationships). Our definition is as follows:
public class DisconnectedUIElementCollection
: UIElementCollection, INotifyCollectionChanged
As required, we derive our class from UIElementCollection
. We also decide to implement INotifyCollectionChanged
to make our collection observable. After all, our owner is going to need some way to monitor changes to the collection. Note that this change notification mechanism is not necessary for the base UIElementCollection
class because its owners can receive change notifications by overriding OnVisualChildrenChanged
()
.
Next, we need an internal collection in which to store our disconnected child elements, so we simply define the following private member:
private Collection<UIElement> _elements
= new Collection<UIElement>();
Keep this _elements
collection in mind because we will refer to it often as we move forward.
Now, it's time to think about our constructor...
We know that we must call the constructor of our base class, UIElementCollection
, which requires two parameters: visualParent
and logicalParent
.
The visualParent
parameter must be a valid UIElement
. It cannot be null
. Internally, the framework works on the assumption that this is the owner of the collection. In fact, the VisualCollection
class actually names it _owner
internally. (Thank you, Lutz Roeder... I still prefer your tool over stepping through the source!)
The logicalParent
parameter can be null
, or it can be any FrameworkElement
. If not null
, the logical parent is very often the same element as the visual parent.
Since we are trying to shirk our parental responsibilities, we're going to play a little trick on the framework when we call the constructor of the base class. (That's perfectly acceptable because I came up with this idea on April Fool's Day!)
Introducing the SurrogateVisualParent Class
Recall that VisualCollection
automatically assigns its members to a visual parent (its owner) as they are added. We don't want our panel to be that owner, so it is clear that we are going to need a "surrogate" visual parent for the panel's children. This surrogate can simply deliver the child to us and be done with it!
Well... as we think about it more... maybe we can use the surrogate parent for more than just delivery... If we develop the surrogate as a very light UIElement
, we can leverage it as an "event sink" to know exactly when children are being added or removed from the VisualCollection
. This can help us solve one of our biggest problems... namely that the framework removes children via its back door access to UIElementCollection
.
To get the required add/remove notifications, we simply need to override OnVisualChildrenChanged()
within the surrogate class. Nice!
For now, let's simply define the surrogate class as follows, and then come back and implement OnVisualChildrenChanged()
later:
private class SurrogateVisualParent : UIElement
{
internal void InitializeOwner(
DisconnectedUIElementCollection owner)
{
_owner = owner;
}
protected override void OnVisualChildrenChanged(
DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
}
private DisconnectedUIElementCollection _owner;
}
Defining the Constructor for DisconnectedUIElementCollection
Now that we have the SurrogateVisualParent
class, we need to expose a public constructor for our custom collection of disconnected UI elements. We are actually going to define this constructor as follows:
public DisconnectedUIElementCollection(UIElement owner)
: this(owner, new SurrogateVisualParent()) {}
Note that our constructor will take a single parameter, which represents the element that owns the disconnected collection. This will eventually be our custom panel, ConceptualPanel
. The reason for passing a reference to the owner will be explained momentarily.
Also note that this constructor creates an instance of our SurrogateVisualParent
class and then defers to another constructor. We can define this other constructor as a private member:
private DisconnectedUIElementCollection(UIElement owner,
SurrogateVisualParent surrogateVisualParent)
: base(surrogateVisualParent, null)
{
_ownerPanel = owner as Panel;
_surrogateVisualParent = surrogateVisualParent;
_surrogateVisualParent.InitializeOwner(this);
}
Here, we are using our private constructor to call the base constructor for UIElementCollection
. Recall that the first parameter of the base constructor specifies the visual parent for children that are added to the internal VisualCollection
. As intended, we are supplying a surrogate visual parent. Then, we specify null
as the second parameter so that no logical parent relationship is established for children in the collection.
Within the body of the private constructor, we store a reference to the collection's owner panel. (If the owner is not a panel, this member will be null
, which is actually perfect for our purposes.) We also store a private reference to our surrogate parent and then initialize that parent so that it has a reference back to the DisconnectedUIElementCollection
.
Overriding the Base Collection Routines
Our next task is to override all of the virtual functions of the base UIElementCollection
class.
First, we need to make sure that any function attempting to access disconnected children is redirected to our private _elements
collection. (Note that we have not yet added any code to populate this collection, but we will certainly do that before we're done.)
To achieve this redirection, we simply implement a lot of "redirecting overrides" that look like the following:
public override int Count
{
get { return _elements.Count; }
}
public override int IndexOf(UIElement element)
{
return _elements.IndexOf(element);
}
I won't include all such functions here, but you definitely get the idea.
The above approach takes care of accessors that return information about our collection. What about accessors that modify our collection? Well, we want these types of accessors to defer to the base class for all modifications. (Recall that we will be monitoring our OnVisualChildrenChanged()
event sink in SurrogateVisualParent
to handle these modifications.)
So, just as we did for the accessors above, we implement a lot of "deferring overrides" that look like this:
public override int Add(UIElement element)
{
return base.Add(element);
}
public override void Insert(int index, UIElement element)
{
base.Insert(index, element);
}
Okay, if we're just going to defer to the base class, why do we need to override the method at all? Well, it turns out, we need to add something to each of these deferring overrides.
Playing Nicely with an Item Container Generator
In our analysis of the base UIElementCollection
class, we discover that certain public methods of the collection effectively become "private" when the owner of the collection is a panel that is acting as the host layout element (a.k.a., the items host) for an ItemsControl
. This is actually an optimization wherein the framework basically assigns ownership of the panel's Children
collection to the item container generator of the ItemsControl
.
If any call is made to directly change a UIElementCollection
while it is under this pseudo-ownership of an item container generator, the framework will throw an exception! Of course, recall that we tricked the framework by specifying a non-Panel
surrogate parent. This means that our calls to the base class methods will actually succeed, even though the true conceptual parent may be a panel. This may seem really cool, but in actuality, we don't want this! Ultimately, if our panel is an items host, this extra freedom could allow our Children
collection to get out of sync with the Items
collection of our ItemsControl
.
Since our ultimate goal is to create a conceptual panel that works in all scenarios, especially where the panel is serving as an items host for an ItemsControl
, we need to ensure that our DisconnectedUIElementCollection
plays by the same rules as the base UIElementCollection
. As such, we need to similarly protect access to all functions that directly modify the internal collection when under ownership of an item container generator.
To achieve this protection, we implement the following routine:
private void VerifyWriteAccess()
{
if (_ownerPanel == null) return;
if (_ownerPanel.IsItemsHost
&& ItemsControl.GetItemsOwner(_ownerPanel) != null)
throw new InvalidOperationException(
"Disconnected children cannot be explicitly added to "
+ "this collection while the panel is serving as an "
+ "items host. However, visual children can be added "
+ "by simply calling the AddVisualChild method.");
}
Note that we added a caveat to our error message that refers to a feature we will add later when we implement the ConceptualPanel
class. We won't worry about that right now.
Now, all we have to do is add a call to VerifyWriteAccess()
at the beginning of any deferring overrides, as shown below:
public override int Add(UIElement element)
{
VerifyWriteAccess();
return base.Add(element);
}
public override void Insert(int index, UIElement element)
{
VerifyWriteAccess();
base.Insert(index, element);
}
Okay, by this time, we have implemented all of the necessary overrides in our DisconnectedUIElementCollection
, and we have safeguarded access to the methods that modify the collection when the owner panel is an items host.
There's still work to be done. We have not yet added any code to ensure that when an item is added to the framework's internal VisualCollection
, we also add it to our private _elements
collection. Moreover, we have not done anything to ensure that the added visuals are unparented.
Monitoring the Visual Children within the Surrogate Parent
In order to populate our _elements
collection, we need to monitor the addition and removal of visual children within the surrogate parent. We will receive these events within our makeshift event sink. Recall that this method has the following signature:
protected override void OnVisualChildrenChanged(
DependencyObject visualAdded, DependencyObject visualRemoved)
Let's first tackle the visualAdded
event. When a child is added to the surrogate parent, we need to also add that child to our _elements
collection. That is certainly easy enough. We can do something like this:
if (visualAdded != null)
{
_owner._elements.Add (visualAdded as UIElement);
}
Of course, we also need to make sure that the added visual no longer has a visual parent. We could simply have the surrogate parent call RemoveVisualChild()
. Unfortunately, this will ultimately cause us grief because VisualCollection
assumes that all of its members are visual children of the supplied owner (the surrogate parent). It will definitely throw an exception if a later call attempts to remove the child from its collection.
Really, the only safe approach for unparenting the child is to remove it from the VisualCollection
entirely and just maintain a reference in our _elements
collection. To do this, we can get the index of the child and then call the RemoveAt()
method of the base UIElementCollection
class. To support that, we need to add private BaseIndexOf()
and BaseRemoveAt()
methods to our DisconnectedUIElementCollection
to provide the SurrogateVisualParent
class access to the base methods of UIElementCollection
:
private int BaseIndexOf(UIElement element)
{
return base.IndexOf(element);
}
private void BaseRemoveAt(int index)
{
base.RemoveAt(index);
}
It is fine to make these private members because the SurrogateVisualParent
class is defined as a private class within DisconnectedUIElementCollection
.
Now, when we call BaseRemoveAt()
within OnVisualChildrenChanged()
, it will actually cause the visual children of our surrogate parent to change again... this time with a removal. Since we plan to use the sink for remove events also, we want to ignore this particular remove event because we caused it ourself. We can simply add an _internalUpdate
flag to our class to avoid this reentrancy. Our updated sink within SurrogateVisualParent
looks something like the following (recall that _owner
is a reference to our DisconnectedUIElementCollection
):
protected override void OnVisualChildrenChanged(
DependencyObject visualAdded, DependencyObject visualRemoved)
{
if (_internalUpdate) return;
_internalUpdate = true;
try
{
if (visualAdded != null)
{
UIElement element = visualAdded as UIElement;
int index = _owner.BaseIndexOf (element);
_owner.BaseRemoveAt(index);
_owner._elements.Add(element);
}
}
finally
{
_internalUpdate = false;
}
}
private bool _internalUpdate = false;
Note that we've taken some liberty in not calling the base OnVisualChildrenChanged()
method, knowing that our ancestors (UIElement
and Visual
) do nothing with this override.
Alright, does anyone see the gaping hole in our approach?
We are working on the assumption that we will be able to use the above method as an event sink for remove operations, but we have just removed the element that we want to track. We can now never receive the desired remove event, should the framework choose to remove the element via its aforementioned back door.
Dealing with Back Door Removals
This is where we start to need some additional knowledge about the internal workings of the framework with respect to UIElementCollection
. Namely, what are the back door methods by which elements might be removed? A little reflection (okay, a lot) reveals that the following methods are the ones that could result in the removal of elements:
internal void ClearInternal();
public virtual void RemoveRange(int index, int count);
internal void SetInternal(int index, UIElement item);
There are other internal back door methods, but they only deal with adding elements, or they only get invoked under UI virtualization scenarios (see the Known Limitations section at the end of this article).
The actual key to our hack can be found in these three internal methods: namely, these are all index-based operations. Okay, it may not be evident that the ClearInternal
method represents an index-based operation, but with deeper reflection on VisualCollection
's Clear
method, it can be seen that the internal collection is merely enumerated by index and each item is disconnected from its visual parent.
Armed with the knowledge that visuals are removed from the internal collection based on their index within the collection, we can cleverly solve the problem of how to track the removal of visual children from the surrogate parent...
Secretly Replacing the Framework's Usual Coffee
We have already used our event sink to remove the newly added child from the base UIElementCollection
. This effectively unparents the child. We have then added the unparented child to our private _elements
collection. Now, we just need to secretly insert some other element in the old child's place within the base collection. This will cause that element to be added as a visual child of our surrogate parent. Then later, when the item container generator decides a child should be removed at a particular index, there will actually be something there to remove.
What should the other element be? It doesn't really matter as long as it's a UIElement
. It would certainly be nice if there were an easy way to relate the replacement element to the element that it is replacing, so we should define a simple UIElement
with a property pointing back to the "sibling" that it represents. For lack of a better name, we can just call the replacement element DegenerateSibling
and define it as follows:
private class DegenerateSibling : UIElement
{
public DegenerateSibling(UIElement element)
{
_element = element;
}
public UIElement Element
{
get { return _element; }
}
private UIElement _element;
}
This is also a private class within DisconnectedUIElementCollection
.
Now, we are able to complete the logic for the visualAdded
event.
if (visualAdded != null:
{
UIElement element = visualAdded as UIElement;
DegenerateSibling sibling = new DegenerateSibling(element);
int index = _owner.BaseIndexOf(element);
_owner.BaseRemoveAt (index);
_owner.BaseInsert(index, sibling);
_owner._degenerateSiblings[element] = sibling;
_owner._elements.Insert(index, element);
_owner.RaiseCollectionChanged(
NotifyCollectionChangedAction.Add, element, index);
}
We are now creating the degenerate sibling, and using a BaseInsert()
method to insert it into the same slot within the base collection from which the actual child was just removed. We also use the Insert()
method to insert the actual child into our private _elements
collection, just to keep the collection perfectly in sync with the collection of degenerate siblings.
Additionally, we are now keeping a dictionary of all degenerate siblings that we create. This is used by our DisconnectedUIElementCollection
class to remove elements by reference (as opposed to removing them by index).
Finally, we are raising a change notification whenever an element is added. As mentioned earlier, this will allow the owner of the DisconnectedUIElementCollection
to respond to changes within the collection.
Now, we just need to add some code to handle the visualRemoved
event. Below is the very straightforward implementation:
if (visualRemoved != null)
{
DegenerateSibling sibling = visualRemoved as DegenerateSibling;
int index = _owner._elements.IndexOf(sibling.Element);
_owner._elements.RemoveAt(index);
_owner.RaiseCollectionChanged(
NotifyCollectionChangedAction.Remove, sibling.Element, index);
_owner._degenerateSiblings.Remove (sibling.Element);
}
When a degenerate sibling is removed, we simply locate the actual child that it represents, and likewise remove it from our private _elements
collection. Of course, we also need to provide a change notification and clean up the reference from the dictionary of degenerates.
That pretty much does it! We now have a custom derivative of UIElementCollection
that can be used to store children of a panel without automatically making them visual children (or logical children) of the panel.
Creating a Panel of Conceptual Children
Now, we just need to create a panel that uses our custom collection for storing its unparented children. This is the easy part! Here is the crux of the class definition:
public abstract class ConceptualPanel : Panel
{
protected override sealed UIElementCollection
CreateUIElementCollection(FrameworkElement logicalParent)
{
DisconnectedUIElementCollection children
= new DisconnectedUIElementCollection(this);
children.CollectionChanged
+= new NotifyCollectionChangedEventHandler
(OnChildrenCollectionChanged);
return children;
}
protected virtual void OnChildAdded(UIElement child)
{
}
protected virtual void OnChildRemoved(UIElement child)
{
}
private void OnChildrenCollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
OnChildAdded (e.NewItems[0] as UIElement);
break;
case NotifyCollectionChangedAction.Remove:
OnChildRemoved(e.OldItems[0] as UIElement);
break;
}
}
}
This class simply overrides CreateUIElementCollection()
to create an instance of our DisconnectedUIElementCollection
. It sets up a handler for monitoring changes to the collection. When a child is added or removed, it calls a virtual method that represents that action. Classes that derive from ConceptualPanel
can simply override OnChildAdded()
and OnChildRemoved()
to respond to the comings and goings of their children.
In the actual ConceptualPanel
implementation, you will note that I have added the following code:
public ConceptualPanel()
{
Loaded += OnLoaded;
}
void OnLoaded(object sender, RoutedEventArgs e)
{
Loaded -= OnLoaded;
(Children as DisconnectedUIElementCollection).Initialize();
}
This just ensures that the disconnected child collection is created at the earliest possible moment during the panel's creation.
We also need to fix the panel's notion of visual children. The base Panel
class assumes that anything within the Children
collection is a visual child. This is no longer true now that our panel uses the DisconnectedUIElementCollection
. Now, those elements are only conceptual children.
For this reason, we must override the VisualChildrenCount
property and the GetVisualChild()
method. To support direct visual children within a ConceptualPanel
, we can add a private _visualChildren
collection and use our overrides to return members of this collection. The following code does this by monitoring OnVisualChildrenChanged()
within the panel:
protected override int VisualChildrenCount
{
get { return _visualChildren.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= _visualChildren.Count)
throw new ArgumentOutOfRangeException();
return _visualChildren[index];
}
protected override void OnVisualChildrenChanged(
DependencyObject visualAdded, DependencyObject visualRemoved)
{
if (visualAdded is Visual)
{
_visualChildren.Add(visualAdded as Visual);
}
if (visualRemoved is Visual)
{
_visualChildren.Remove(visualRemoved as Visual);
}
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
}
private readonly List<Visual> _visualChildren = new List<Visual>();
That does it for the ConceptualPanel
implementation.
Creating a Panel of Logical Children
Very often, you might want the children within the panel to be logical children of the panel, but not visual children. This will allow things like resource resolution and property inheritance, which work through the logical tree, to seamlessly work with the panel's children. For this reason, we can create a LogicalPanel
class that derives from ConceptualPanel
, as follows:
public abstract class LogicalPanel : ConceptualPanel
{
protected sealed override void OnChildAdded(UIElement child)
{
if (LogicalTreeHelper.GetParent(child) == null)
{
AddLogicalChild(child);
}
OnLogicalChildrenChanged(child, null);
}
protected sealed override void OnChildRemoved(UIElement child)
{
if (LogicalTreeHelper.GetParent(child) == this)
{
RemoveLogicalChild(child);
}
OnLogicalChildrenChanged(null, child);
}
protected virtual void OnLogicalChildrenChanged(
UIElement childAdded, UIElement childRemoved)
{
}
}
This class provides a virtual function called OnLogicalChildrenChanged()
that is intentionally similar to the OnVisualChildrenChanged()
function. Derived panels can override this method to respond to the adding or removing of logical children.
It's probably worth mentioning that this method only announces the arrival or departure of logical children that belong to the Children
collection. This means it applies only to UIElement
s. Any other logical child that is explicitly added or removed from the panel will not cause the execution of the OnLogicalChildrenChanged()
method.
That's it! Now we have a purely logical panel.
It's always good to know your limits. Below are some known limits of the current implementation of these classes. I'm sure I haven't thought of everything, so I will update this section if/when other limitations are brought to my attention.
1. Panel.ZIndex is not respected for visual children of a ConceptualPanel
This isn't that big of a deal, since a ConceptualPanel
doesn't even have visual children by default, but it at least merits mention. If you add visual children to a ConceptualPanel
via AddVisualChild()
and then set the Panel.ZIndex
property on those children, it will have no effect. As with non-panel elements, the order in which visual children are returned during an enumeration is entirely based on the order in which they are added via AddVisualChild()
. Z-ordering could certainly be added, but since a ConceptualPanel
may not even have visual children, I did not feel it warranted the extra work.
2. ConceptualPanel does not provide for UI virtualization
ConceptualPanel
derives from Panel
, so there is no inherent UI virtualization when it is acting as an items host. All generated item containers will belong to the Children
collection. As such, you will pay a memory price for each container, as well as a processing price for the CPU cycles required to instantiate the container and its visuals. You will not, however, pay a render price for the conceptual children unless you explicitly add them to the visual tree.
Theoretically, you could write a ConceptualVirtualizingPanel
class that derives from VirtualizingPanel
and uses DisconnectedUIElementCollection
to hold its children. Then, you would have a virtualizing panel with disconnected children. However, a virtualizing panel tends to recycle item containers and move containers around in the internal VisualCollection
. There are a couple of additional back door methods exposed by UIElementCollection
(MoveVisualChild()
and RemoveNoVerify()
) that come into play under these scenarios. These operations modify the collection based on element references rather than element indices. That would likely break the DisconnectedUIElementCollection
approach.
Clearly, there is some additional investigation that should happen if a conceptual virtualizing panel is needed. I leave that as an exercise for a more ambitious hacker.
3. A child of a ConceptualPanel may still have a logical parent
This architecture only ensures that the ConceptualPanel
, itself, will not be the logical parent of its children. It is still certainly possible that a child with a different logical parent will be added to the Children
collection. The scenario in which this will occur most often is when the ConceptualPanel
is an items host and a child that qualifies to be an item container is added to the Items
collection of the controlling ItemsControl
.
Any object that is added to the Items
collection of an ItemsControl
becomes a logical child of that ItemsControl
. This is part of the ItemsControl
content model, as described in 'D' is for DataTemplate. This means that if an item container is added directly to the Items
collection (as opposed to being generated by the ItemContainerGenerator
of the ItemsControl
), it will already be a logical child of the ItemsControl
when it is added to the ConceptualPanel
. It is not possible to sever this relationship from within the panel (unless you revert to calling an internal framework method, and even I don't recommend that level of hackery).
For a list of ItemsControl
classes and their respective item containers, see the table at the end of 'I' is for Item Container.
With Josh’s permission, I have updated his Panel3D
class within the WPF Disciples Blogroll application to derive from LogicalPanel
.
You can download the code from the link provided at the top of this article.
This package includes the full source code for my DisconnectedUIElementCollection
, ConceptualPanel
, and LogicalPanel
classes. Note that in addition to my changes, this sample contains several improvements that were added by Josh, himself. (Clearly, he considers this to be a work in progress, and I know we’re all looking forward to seeing where it leads!)
In addition to changing the base class to LogicalPanel
, I also removed the Viewport3D
element from the adorner layer and added it as a direct visual child of the Panel3D
element. The viewport is now the only visual child of the panel. As a result of deriving from LogicalPanel
, all of the elements within the panel’s Children
collection are merely logical children of the panel. This allows them to be used as direct visual children of their respective Viewport2DVisual3D
elements within the 3D scene.
I hope others find this new concept of "conceptual children" as cool as I do!