Welcome to WindowsClient.net | Sign in | Join
in

WindowsClient.net

This Blog

Syndication

Sponsors





Tags

No tags have been created or used yet.

Archives

Bragi

DistributionPanel

Intro

Let me start by telling you that this post was actually written about 40 days ago.  Non finished code kept me from posting it, which I found a real shame. Especially, since the technique it is based on got me very excited. But I simply couldn't get me to post some code, I hadn't even been able to test yet, which I am sure you understand.  So what exactly got me so excited anyway?  Well, simply put: this.  It's an article from Dr WPF about a new panel he made called 'ConceptualPanel' which  detaches the items in the panel from it's visual tree so that they can be placed somewhere else. When I read this, I had just started looking into a similar feature (well, actually I had already begun coding it for a couple of days), but had approached my problem from a different angle: I presumed that Panels couldn't be changed in the way Dr WPF did (thank you for proving me wrong on this one), so I figured to re-implement most of the ItemsControl features but in such a way that you could send them to multiple panels.  This proved to be a nightmare to put it mildly due to the many internal functions used by the ItemsControl. All situations could be solved, but not without breaking with the logic used by the .net framework. So this article really came as a blessing and saved my but.

What does DistributionPanel do?

First of though: What really is the importance of this conceptual panel?  Well, from my point of view it all becomes very interesting when you start using this panel with an ItemsControl.  You see, in the past, whatever you put on an ItemsControl, had to remain in its visual tree (in other words, inside the ItemsControl) because this uses a Panel to perform it's layout and those automatically made all their Children also visual children.  This is no longer true, you can now put the containers anywhere you want, even on another window.  So the ItemsControl simply becomes a factory of container elements.  Of course, ConceptualPanel only gives you the detaching, it doesn't actually provide a way to use multiple destinations for it's children (which is the feature that I was looking for). As you probably already guessed by know, this is where DistributionPanel steps in.  It expands the ConceptualPanel so that it can distribute it's children to other lists without using code (so you can do it all from xaml).

Yes, you read it correctly, and it isn't a typo or expression error (something I can't always claim to be true), DistributionPanel can add it's children to other lists through the IList interface - not just to Panels.  It can also remove them again when the child is removed.  Using the IList interface instead of Panels can be useful in cases where you want to put the items on something different, for instance, you can create a class that makes all items in the list floating. And since the UIElementCollection used by the Panel also implements IList, you can still use panels as distribution targets.

Key and Targets

Distribution is performed by using a Key and a list of Targets.  The key is assigned to each item in the list using an attached property. Each DistributionTarget declares a value that is compared to the key of each item that needs to be distributed.  The first target who's value matches with the key will be used to put the item in.  This is done by adding the item to an IList that can be found through the Target property of the DistributionTarget.

Ouch, sounds complicated.  Actually it really isn't.  I think a code sample is appropriate by know to clear things up.

<DockPanel>
   <jc:DistributionPanel>
      <jc:
DistributionPanel.Targets> <jc:DistributionTarget Value="Top"
Target="{Binding ElementName=PART_Top, Path=Children}" /> <jc:DistributionTarget Value="Left"
Target="{Binding ElementName=PART_Left, Path=Children}"/> <jc:DistributionTarget Value="Right"
Target="{Binding ElementName=PART_Right, Path=Children}"/> <jc:DistributionTarget Value="Bottom"
Target="{Binding ElementName=PART_Bottom, Path=Items}"/> </jc:DistributionPanel.Targets> <TextBlock jc:DistributionPanel.Key="Bottom">Item A</TextBlock> <TextBlock jc:DistributionPanel.Key="Top">Item B</TextBlock> <TextBlock jc:DistributionPanel.Key="Left">Item C</TextBlock> <TextBlock jc:DistributionPanel.Key="Right">Item D</TextBlock> </jc:DistributionPanel> <StackPanel DockPanel.Dock="Top" x:Name="PART_Top"/> <ListBox DockPanel.Dock="Bottom" x:Name="PART_Bottom" /> <WrapPanel DockPanel.Dock="Left" x:Name="PART_Left"/> <self:CustomList/> </DockPanel>

Basically, this is a DockPanel which has a stackPanel at the top, a listbox in the bottom, a WrapPanel to the left and some custom list object in the middle.  The DistributionPanel contains all the items which will be distributed to all the other items found in the DockPanel. Each item in the list has an attached property DistributionPanel.Key.  The DistributionPanel also has a list of targets. The first one will receive all the items in the distributionPanel who's key value equals "Top" and sends the items to the StackPanel at the bottom of the app.  The second target filters out all the items which have a Key value of 'Left', and so on.

Uses

Even though the distribution technique appears very simple, you can get some really cool results, especially if you combine it with the triggers. Lets say there are 2 properties that determine where an item needs to be placed.  You can use a DataTrigger that checks the value of these 2 properties and use a setter to assign the correct value to DistributionPanel.Key.

As already mentioned, this class is probably most useful when used as the ItemsPanel of an ItemsControl (such as a ListBox or TreeView).  This way, you can easily detach the items in the ItemsControl and put them somewhere else, like during a drag operation.

Extending/changing the distribution

DistributionPanel has been designed with extensibility in mind.  Interesting  new features could be: support for animation during a transition of one distribution location to another one, or you could implement a placeholder kind of functionality where the panel is able to put an item back in it's original location (The current implementation of DistributionPanel looses the order of items when it moves items from one list to another).

To extend the class, you should overwrite the protected method Distribute, which is called whenever an item is added, removed or moved from one target to another. So, if you would want to add animation functionality, this is the place to do it. All that the method does is raise the appropriate events and ask the DistributionTarget objects to add/remove the item.  Here's the implementation:

protected virtual void Distribute(UIElement item, DistributionTarget oldTarget,
         DistributionTarget newTarget)
{
   //raise the preview event
   DistributionEventArgs iArgs = RaisePreviewDistributedEvent(item, oldTarget, newTarget);
   //do some sanity check: if there is an oldTarget, it should be in it's list, otherwise
   //we shouldn't be able to remove it.
   //Also, if there is an oldTarget, ask it to remove the item.
   if (oldTarget != null)
   {
      if (oldTarget.Items.Contains(item) == true)
         oldTarget.RemoveChildFromTarget(item);
      else
         throw new DistributionException("Invalid old key!", new ArgumentOutOfRangeException("oldTarget"));
   }
   //if there is a new target, ask it to add the item.
   if (newTarget != null)
      newTarget.AddChildToTarget(item);
   //and finally raise the event indicating that the distribution has completed.
   RaiseDistributedEvent(iArgs);
}

For animation, you will probably have to re-implement this method, that is, if you want the Distributed event to be raised after the animation is done. The panel also has the Redistribute (called only when an item is moved from one location to another), RebuildDistribution (recreates the entire distribution) and ClearDistributionFor (removes all the items from a specific target list) virtual methods that you can overwrite, but scenario's to overwrite these are not that abundant.

Besides DistributionPanel itself, you can also modify the RemoveChildFromTarget and AddChildToTarget protected methods defined in the DistributionTarget class. These methods do the actual adding and removing from the target lists.  Here are the default implementations:

internal protected virtual void AddChildToTarget(UIElement item)
{
   IList iList = Target;
   if (iList != null)
      iList.Add(item);
   //Items is a protected property that contains all the UIElements
   //assigned to this target.
   Items.Add(item);
}
internal protected virtual void RemoveChildFromTarget(UIElement item)
{
   IList iList = Target;   
   if (iList != null)
      iList.Remove(item);
   //Items is a protected property that contains all the UIElements
   //assigned to this target.
   Items.Remove(item);
}

As you can see, nothing fancy, just a standard add or remove from the target list. The only thing worth noting is the second list: DistributionTarget also maintains an internal list containing all the items that it maintains.  This is used in case there is no target list assigned. So this list must also be kept in sync. You can easily extend these 2 functions by replacing the item with a dummy instead of removing it from the list, which allows you to put the item back at it's original location.

It is my opinion that DistributionPanel provides the foundations for some interesting Controls. I hope you find good use for it, and I would be glad if you'd let me know about the cool stuff you did with it. The panel is part of my ControlFramework library, which you can download from here.

And as a final note there are still the:

Limitations and restrictions

All the limitations of the ConceptualPanel on which it is based are also valid for this panel, so items can loose their order (especially when they are redistributed from one list to another, because items are always added to the back of the new target list) and if microsoft decides to change the internals of their  panels, this could brake. 

In addition, you should keep in mind that a DistributionPanel can't be used in a cascade, so you can't send the content of one DistributionPanel to another one.  Other than that, you are free to do with it whatever you want.

Comments

No Comments

Leave a Comment

(required)  
(optional)
(required)  
Add
Page view counter