|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionHave 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 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 He naturally arrived at the idea that he could override 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 Clones in SpaceThis 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 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 ProblemsWatching 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).
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! 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 Both of these problems stem from the fact that a panel's children are maintained within a special collection of Side note: If you are curious, the 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 Obviously, we should still be able to use the panel as an items host. This means that its ConceptualPanel, LogicalPanel, and DisconnectedUIElementCollectionThe rest of this article contains the details regarding my implementation of a couple of new panels called 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 DetailsFor 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 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 There's the disclaimer. Now, let's begin our attack! A Brief Look at UIElementCollectionTo really understand how the children of a typical panel are maintained, we must first understand how 1. The Children property of a Panel is of type UIElementCollectionThe 2. UIElementCollection provides an extremely handy way to provide any element, not just a panel, with a collection of visual (and optionally, logical) childrenAn element can simply create an instance of 3. UIElementCollection stores its members in a VisualCollectionInternally, a 4. UIElementCollection behaves differently when owned by a PanelWhen it is used to host the children of an "items host", 5. UIElementCollection has a back doorThere are special internal methods on 6. UIElementCollection pretends to be extensibleThe 7. UIElementCollection is not really extensibleAlthough it is true that we can derive and use our own custom 8. We are going to extend UIElementCollection anyway!It should now be clear that the framework doesn't want us mucking with the internal Remember our objective? We want to create a derivative of the 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 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 ClassBy now, I think it's evident that any solution we build needs to be based on a custom public class DisconnectedUIElementCollection
: UIElementCollection, INotifyCollectionChanged
As required, we derive our class from 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 Now, it's time to think about our constructor... We know that we must call the constructor of our base class, The The 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 ClassRecall that 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 To get the required add/remove notifications, we simply need to override For now, let's simply define the surrogate class as follows, and then come back and implement 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 DisconnectedUIElementCollectionNow that we have the 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, Also note that this constructor creates an instance of our 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 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 Overriding the Base Collection RoutinesOur next task is to override all of the virtual functions of the base First, we need to make sure that any function attempting to access disconnected children is redirected to our private 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 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 GeneratorIn our analysis of the base If any call is made to directly change a 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 To achieve this protection, we implement the following routine: private void VerifyWriteAccess()
{
// if the owner is not a panel, just return
if (_ownerPanel == null) return;
// check whether the owner is an items host for an ItemsControl
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 Now, all we have to do is add a call to 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 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 Monitoring the Visual Children within the Surrogate ParentIn order to populate our protected override void OnVisualChildrenChanged(
DependencyObject visualAdded, DependencyObject visualRemoved)
Let's first tackle the 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 Really, the only safe approach for unparenting the child is to remove it from the 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 Now, when we call protected override void OnVisualChildrenChanged(
DependencyObject visualAdded, DependencyObject visualRemoved)
{
// avoid reentrancy during internal updates
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 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 RemovalsThis is where we start to need some additional knowledge about the internal workings of the framework with respect to 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 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 CoffeeWe have already used our event sink to remove the newly added child from the base What should the other element be? It doesn't really matter as long as it's a 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 Now, we are able to complete the logic for the 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 Additionally, we are now keeping a dictionary of all degenerate siblings that we create. This is used by our Finally, we are raising a change notification whenever an element is added. As mentioned earlier, this will allow the owner of the Now, we just need to add some code to handle the 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 That pretty much does it! We now have a custom derivative of Creating a Panel of Conceptual ChildrenNow, 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 In the actual 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 For this reason, we must override the 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 Creating a Panel of Logical ChildrenVery 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 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 It's probably worth mentioning that this method only announces the arrival or departure of logical children that belong to the That's it! Now we have a purely logical panel. Known Limitations of the ConceptualPanel ArchitectureIt'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 ConceptualPanelThis isn't that big of a deal, since a 2. ConceptualPanel does not provide for UI virtualization
Theoretically, you could write a 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 parentThis architecture only ensures that the Any object that is added to the For a list of Seeing is BelievingWith Josh’s permission, I have updated his
You can download the code from the link provided at the top of this article. This package includes the full source code for my In addition to changing the base class to I hope others find this new concept of "conceptual children" as cool as I do!
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||