This is Part 4 of my series of posts dedicated to creating an animating and virtualizing WrapPanel for Silverlight. If you missed the previous posts, I suggest you take a look.
Virtualization
“Virtualization” – does anyone know what that means? I imagine there are quite a few different answers, but it means two things to me. The first is that we are only creating enough UserControls to represent the visible items. The second is that these controls are not continuously destroyed and created as items scroll into and out of view, but rather are “recycled” as content presenting containers with only their content changing with item visibility.
While the setup to create a virtualizing control can take some extra effort, it is vital when dealing with large numbers of items. Without virtualization the processing power required to create, measure and arrange thousands of controls becomes computationally prohibitive and can cause your application to slowdown, freeze or crash.
Note that while I did write this example based on my work for Gadfly (check it out!), much of my information and source originally came from Bea Stollnitz, Dr. WPF, Silverlight.FX, MSDN and other sources. Also, I will be using explanations where I’d ordinarily use code snippets to save space and simplify concepts. The full source can be downloaded with the usual caveat that it’s provided “as-is” without express or implicit warrantee or support.
Setup
ItemsControl
In order to virtualize our panel we have to use the panel as the ItemsPanelTemplate of an ItemsControl. This gives us access to the ItemContainerGenerator which is essential for virtualization. It also allows us to create a ControlTemplate for the ItemsControl in such a way that we can capture the ScrollViewer to explicitly control scrolling.
The style is simply defined in the Page.xaml resources as:
<UserControl.Resources>
<Style x:Key="ItemsControl" TargetType="ItemsControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer>
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
and the ItemsControl itself (which is bound to a large ObservableCollection of ints called “Numbers”)
<ItemsControl x:Name="itemsControl" Style="{StaticResource ItemsControl}" ItemsSource="{Binding Numbers}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:NumberBox />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:VirtualizingWrapPanel Margin="1" Background="White" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Virtualization and Scrolling
General Info
I’m tackling Virtualization and Scrolling together here because they are interrelated. In order to virtualize our panel we have to track the visible controls – which means tracking the ScrollViewer Viewport, Extent and Offset values. But it also means that we need a repeatable way to calculate the indices of the first visible item and the last visible item.
Recycling containers means that we will have to keep our own internal collection of visible (or “realized”) children separate from the panel Children collection. The ItemsControl.ItemContrainerGenerator will handle a lot of the heavy lifting for us. As we will see in the code later, the Panel.Children will hold available ContentPresenters while our _realizedChildren collection holds the ContentPresenters with visible content (our Numbers). This provides a framework for disassociating the ContentPresenters from their Content without destroying them, as well as associating them with different Content later instead of creating new controls, saving the overhead of instantiating and disposing the UIElements.
If you download the source you’ll see I’ve got some fancy scrolling. It’s a smooth scrolling behavior which animates the vertical offset. To do this I had to implement the IScrollInfo interface. One important piece for virtualization is that every time we update the scroll offset we call InvalidateMeasure() on the panel to ensure that each control moves correctly, recycling occurs and items scrolling into view are realized.
MeasureOverride
The MeasureOverride method from my previous posts needs a facelift in order to make virtualization work. Before we get into the measure logic itself we have to update the scroll info, recalculate the visible control indices, recycle containers that have moved out of the visible range, realize containers for items that have scrolled into view, and keep our _realizedChildren and Panel.Children collections in sync. Then, once the measuring is done, we need to clean up the Children collection to remove unused recycled containers. Easy as 1-2-3…crap. Ok, let’s break it down.
Update Scroll Info
This is actually pretty straightforward. We pass in the availableSize parameter from the MeasureOverride call and use it as the ScrollViewer Viewport. It is also used to calculate the ScrollViewer Extent. In order to keep this simple we will use an assumed size for the control. In practice (as in Gadfly) these controls could have different sizes and additional calculations may be necessary. (The GetItemsCount() method returns the count from the ItemsControl.Items – so ALL items.) For our WrapPanel the Extent is calculated as:
private Size MeasureExtent(Size availableSize)
{
int itemCount = GetItemsCount();
double colCount = Math.Floor(availableSize.Width / _assumedControlSize.Width);
colCount = Math.Min(colCount, itemCount);
colCount = colCount >= 1 ? colCount : 1;
double rowCount = Math.Ceiling(itemCount / colCount);
Size result = new Size(colCount * _assumedControlSize.Width,
(rowCount * _assumedControlSize.Height));
return result;
}
Once the extent and viewport are set InvalidateScrollInfo() is called.
Calculate Visible Indices
This is an essential part of virtualization – without it you cannot limit the realized controls and your in-memory children can grow to enormous sizes. Using the ScrollViewer Viewport and assumed control size we can determine how many rows and columns are visible. Then, using the ScrollViewer VerticalOffset we can determine the first visible index. And from there we can use the visible rows and columns to get the last visible index. This logic is based on the WrapPanel nature of our layout and would need to adjust based on how your items are arranged. In this case I also adjust the visible indices up and down one row of controls to ensure that partial items show during smooth scrolling.
Recycle Containers
Now that we now what should be visible, we go through our realized children and see if any are outside the index range. Remember that the visible indices are based on the full Items collection and not the children collections. This is where we get to use the ItemContainerGenerator for the first time. It provides us with a way to iterate through the full list of Items based on GeneratorPosition as well as a single method call for Recycling containers.
private void RecycleContainers()
{
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int recycleRangeStart = -1;
int recycleRangeCount = 0;
int childCount = _realizedChildren.Count;
for (int i = 0; i < childCount; i++)
{
bool recycleContainer = false;
int itemIndex = itemsControl.Items.IndexOf(
(_realizedChildren[i] as ContentPresenter).Content);
if (itemIndex >= 0 && (itemIndex < _startPosition || itemIndex > _endPosition))
{
recycleContainer = true;
}
if (recycleContainer)
{
if (recycleRangeStart == -1)
{
recycleRangeStart = i;
recycleRangeCount = 1;
}
else
{
recycleRangeCount++;
}
}
else
{
if (recycleRangeCount > 0)
{
GeneratorPosition position = new GeneratorPosition(recycleRangeStart, 0);
((IRecyclingItemContainerGenerator)_generator).Recycle(
position, recycleRangeCount);
_realizedChildren.RemoveRange(recycleRangeStart, recycleRangeCount);
childCount -= recycleRangeCount;
i -= recycleRangeCount;
recycleRangeCount = 0;
recycleRangeStart = -1;
}
}
}
if (recycleRangeCount > 0)
{
GeneratorPosition position = new GeneratorPosition(recycleRangeStart, 0);
((IRecyclingItemContainerGenerator)_generator).Recycle(
position, recycleRangeCount);
_realizedChildren.RemoveRange(recycleRangeStart, recycleRangeCount);
}
}
Realize Containers and Update Children
OK, so now we need to associate the newly visible items with a container – either new or recycled. Once again the ItemContainerGenerator does a lot of this work for us, we just have to help it along. So, we’ll tell it where to start and when to stop and have it iterate through each visible item (the content, remember) and create the ContentPresenter container for us. Calling GenerateNext does exactly that, returning the ContentPresenter with it’s content all matched up nicely. It also has an out parameter to indicate whether the ContentPresenter had to be created or was taken from the pool of recycled containers.
GeneratorPosition start = _generator.GeneratorPositionFromIndex(_startPosition);
int childIndex = (start.Offset == 0) ? start.Index : start.Index + 1;
using (_generator.StartAt(start, GeneratorDirection.Forward, true))
{
for (int i = _startPosition; i <= _endPosition; ++i)
{
bool isNewlyRealized;
UIElement child = _generator.GenerateNext(out isNewlyRealized) as UIElement;
if (child == null) continue;
if (isNewlyRealized)
{
InsertContainer(childIndex, child, false);
}
else
{
if (childIndex >= _realizedChildren.Count ||
!(_realizedChildren[childIndex] == child))
{
InsertContainer(childIndex, child, true);
}
}
childIndex++;
#region Measure Logic
}
}
Now we have to update our _realizedChildren and Children collections. If the item is newly realized it means it’s a brand new control that just has to be inserted into the correct location. If it is not newly realized there are two options: first that the child was already realized and it’s index hasn’t changed, or second that it’s a recycled container in a different location. In the first case we have no more work, but in the second we have to remove the container from it’s old location and insert it into the new location properly. All of this work relies on methods defined by the abstract VirtualizingPanel base class, from which our panel now inherits.
There is some slightly sticky logic here to determine the proper index to remove from and insert at and I’m going to ignore it here. The InsertContainer method is commented in the downloadable source, but feel free to comment or contact me if you have questions.
Clean Up Unused Children Containers
At this point the Children collection could potentially contain unused recycled containers. These are maintained inside the ItemContainerGenerator for future use and should not be kept in the Panel.Children collection. Iterating through the Children and comparing each element in order to the _realizedChildren quickly locates unnecessary containers that we can remove via the VirtualizingPanel.RemoveInternalChildRange method.
ArrangeOverride
So, that was a lot of work, but here comes the easy part. Because we’ve handled it all in the MeasureOverride there’s only one little change here – which is to call UpdateScrollInfo at the beginning of the method.
Conclusion
Let’s put it to the test. I’ve created a list of 10,000 integers and bound two ItemsControls to that list. The left is my VirtualizingWrapPanel, and the right is a standard StackPanel. Notice the difference in scrolling performance between the two seen here with the simplest of data models and user controls. Imagine the difference it can make with a much more complicated schema. Happy Virtualizing.
This is Part 3 of my series of posts dedicated to creating an animating and virtualizing WrapPanel for Silverlight. If you missed the previous posts, I suggest you take a look.
Setup
NumberBox Animation
Let’s make this easy on ourselves by having the child control handle it’s own animation. To do this I’ve added an animation to my NumberBox resources called "slide.”
<Storyboard x:Key="slide">
<DoubleAnimation To="0" BeginTime="0:0:0" Duration="0:0:0.5"
Storyboard.TargetName="LayoutRoot"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(TranslateTransform.X)" />
<DoubleAnimation To="0" BeginTime="0:0:0" Duration="0:0:0.5"
Storyboard.TargetName="LayoutRoot"
Storyboard.TargetProperty="(UIElement.RenderTransform).
(TransformGroup.Children)[0].(TranslateTransform.Y)" />
</Storyboard>
You’ll notice there isn’t anything special about this Storyboard, it’s a simple TranslateTransform animation. Oh, make sure you’ve got your RenderTransform defined too.
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RenderTransform>
<TransformGroup>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Grid.RenderTransform>
I’ve created this animation as a resource for my UserControl, but it could just as easily live in the application level resource dictionary, or be created entirely in code behind. But, now that we’ve got the Storyboard in place, let’s give callers the ability to pick a destination and start the animation. For that I’ve created a public method which takes two parameters to define the translate transform values like this:
public void SlideTo(double x, double y)
{
Storyboard slideAni = Resources["slide"] as Storyboard;
(slideAni.Children[0] as DoubleAnimation).To = x;
(slideAni.Children[1] as DoubleAnimation).To = y;
slideAni.Begin();
}
ArrangeOverride
Animation
Now that we are going to animate the location of our panel’s children we will need to update the ArrangeOverride method. First of all, even though we are going to animate the location of the child control, the logic required to determine that location does not change. Secondly, we still need to call the UIElement.Arrange method for each control to ensure that it will be shown.
WrapPanel
Our WrapPanel will call the NumberBox.SlideTo method to animate the control. We do not ever set the From values for that animation, necessitating that the NumberBox always be Arranged to the same location and that each transform be relative to that same location. To keep things simple we will arrange every child to (0,0) and then animate the TranslateTransform. That makes our ArrangeOverride code look like this:
protected override Size ArrangeOverride(Size finalSize)
{
Size sizeSoFar = new Size(0, 0);
foreach (UIElement child in Children)
{
child.Arrange(new Rect(0, 0, child.DesiredSize.Width, child.DesiredSize.Height));
(child as NumberBox).SlideTo(sizeSoFar.Width, sizeSoFar.Height);
if (sizeSoFar.Width + child.DesiredSize.Width >= finalSize.Width)
{
sizeSoFar.Height += child.DesiredSize.Height;
sizeSoFar.Width = 0;
}
else
{
sizeSoFar.Width += child.DesiredSize.Width;
}
}
return finalSize;
}
So, with a simple control-level animation and two changes to our ArrangeOverride we are now animating our WrapPanel! Easy!
Next Steps
This has been a simplistic example to prove we can animate our custom panel. A more realistic, real-world scenario is using our panel as the ItemsPanelTemplate of an ItemsControl bound to a collection of data objects. For the next post we will move to using an ItemsControl and discuss the need for and benefits of Virtualization.
This is Part 2 of my series of posts dedicated to creating an animating and virtualizing WrapPanel for Silverlight. If you missed the Introduction or Part 1 I suggest you take a look.
Setup
WrapPanel
We’ve already created the WrapPanel class in Part 1, so now all we need to do is add the ArrangeOverride method to the class.
protected override Size ArrangeOverride(Size finalSize)
{
return base.ArrangeOverride(finalSize);
}
ArrangeOverride
General Info
The ArrangeOverride method takes a Size parameter and returns a Size. The finalSize parameter represents the space allotted to the panel and the Width and Height values range from 0 to positive infinity. The finalSize parameter is determined by the MeasureOverride availableSize parameter and return value using the following process (defined for Width, but equivalent for the Height also).
If the Panel Width, MinWidth and MaxWidth are NaN (i.e. not explicitly set) and the HorizontalAlignment is Center, Left or Right then the Width value is from the MeasureOverride return value. If the Width values are NaN and HorizontalAlignment is Stretch then the Width is the maximum of the MeasureOverride availableSize width and the return value’s width property. If, however, the Width is set, the finalSize width is the maximum of the Width and the return value’s Width.
The ArrangeOverride return value is the size that is then used as the RenderSize. Typically, the finalSize is just returned regardless of the logic in the method.
The guts of the ArrangeOverride method is to physically place each child in the appropriate location. This is accomplished using the UIElement.Arrange(Rect) method. The Rect parameter passed into Arrange defines the X and Y coordinates of the top left corner of the child, relative to the parent, while the Width and Height values define the Width and Height of the area the child element should take up.
In my experience the ArrangeOverride method contains similar logic to the MeasureOverride as you have to iterate through the children and place them, tracking how much space has been used as you go along. The main difference is that UIElement.Measure is called in MeasureOverride and UIElement.Arrange is called in ArrangeOverride.
WrapPanel
For our panel the ArrangeOverride method will simply arrange each child element into the appropriate location using the same wrapping logic described in Part 1. The differences are that we don’t have to track the maximum width and, as mentioned above, we call UIElement.Arrange instead of UIElement.Measure.
protected override Size ArrangeOverride(Size finalSize)
{
Size sizeSoFar = new Size(0, 0);
foreach (UIElement child in Children)
{
child.Arrange(new Rect(sizeSoFar.Width, sizeSoFar.Height,
child.DesiredSize.Width, child.DesiredSize.Height));
if (sizeSoFar.Width + child.DesiredSize.Width >= finalSize.Width)
{
sizeSoFar.Height += child.DesiredSize.Height;
sizeSoFar.Width = 0;
}
else
{
sizeSoFar.Width += child.DesiredSize.Width;
}
}
return finalSize;
}
The same assumptions we made in Part 1 still apply here.
Calling Arrange
So, when does ArrangeOverride get called? Well, every time MeasureOverride is called ArrangeOverride is called after. So we can force an Arrange by calling UIElement.InvalidateMeasure(). However, if we know the measure is accurate and we want to skip what can be a complex process we can call UIElement.InvalidateArrange() directly instead. Also, just as with MeasureOverride, a custom defined DependencyProperty can register such that it includes FrameworkPropertyMetadataOptions.AffectsArrange (WPF only).
Completing the Example
So now our WrapPanel implementation has a MeasureOverride and ArrangeOverride and is complete. We built our test control (NumberBox) in Part 1 – so that’s ready to go. All we need now is to add the WrapPanel to a parent and add some code to add and remove children.
When creating a new Silverlight 3 project in Visual Studio I asked it to create a test page to host the Silverlight at build time. This UserControl is autocreated as Page.xaml and is has straightforward XAML:
<UserControl x:Class="Clarity.Demo.CustomPanel.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Clarity.Demo.CustomPanel"
MaxWidth="500" MaxHeight="500">
<Grid x:Name="LayoutRoot" Background="White">
<local:WrapPanel x:Name="wrapPanel" />
</Grid>
</UserControl>
Don’t forget to reference your local namespace in order to get to your WrapPanel control. Next we’ll create a DispatcherTimer to add children to our WrapPanel. I implemented this in the Page constructor, but it a real-life scenario would life be populated via binding or user interaction. I place each new child control at the beginning of the Children collection so you can see each control is arranged to it’s new position with the addition of each subsequent child. Also, for the sake of seeing our example a little cleaner, I clear the Children when we’ve got 50 of them.
public partial class Page : UserControl
{
private DispatcherTimer _timer = new DispatcherTimer();
private int _count = 0;
public Page()
{
InitializeComponent();
PopulatePanel();
}
private void PopulatePanel()
{
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (sender, args) =>
{
if (_count > 50)
{
wrapPanel.Children.Clear();
_count = 0;
}
wrapPanel.Children.Insert(0, (new NumberBox(_count++)));
};
_timer.Start();
}
}
And this is what it looks like:
OK, admittedly this is kind of lame. The real fun starts in the next post when we will animate the controls as they rearrange.
This is Part 1 of my series of posts dedicated to creating an animating and virtualizing WrapPanel for Silverlight. If you missed the Introduction I suggest you take a look.
Before we write any code for our custom panel we have to decide what the layout of the panel’s children should be. For this example, I have decided to create a WrapPanel. (Yes, I know there is already a WrapPanel in the Silverlight Toolkit, but this one will be cooler, I promise.) We will use the WPF WrapPanel default behavior of laying out items left to right, top to bottom.
Setup
Child Control
Before we get into the MeasureOverride details let’s take a quick look at what we’ll be adding to our panel. I’ve created a Silverlight 3 UserControl called NumberBox which is just a Border and a TextBlock so we can differentiate the children.
<UserControl x:Class="Clarity.Demo.CustomPanel.NumberBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="50" Height="50">
<Grid x:Name="LayoutRoot" Background="White">
<Border x:Name="border" BorderBrush="Red" BorderThickness="2" Margin="5">
<TextBlock x:Name="text" Text="0" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="16"/>
</Border>
</Grid>
</UserControl>
public partial class NumberBox : UserControl
{
public NumberBox()
{
InitializeComponent();
}
public NumberBox(int number)
{
InitializeComponent();
text.Text = number.ToString();
}
}
This is a simple control that we will see put to use in Part 2: ArrangeOverride.
WrapPanel
Next we set up a new class WrapPanel which inherits from Panel and overrides the base MeasureOverride method:
public class WrapPanel : Panel
{
public WrapPanel() : base () {}
protected override Size MeasureOverride(Size availableSize)
{
return base.MeasureOverride(availableSize);
}
}
MeasureOverride
General Info
The MeasureOverride method takes a Size parameter and returns a Size. The availableSize parameter represents the space allotted to the panel by it’s parent and values range from 0 to positive infinity for both the Width and Height properties.
If the panel is contained in a specific size you will see availableSize with those values. This applies not just for explicitly set Width and Height values, but also when the MinWidth/MinHeight and MaxWidth/MaxHeight properties are set. If the panel has margins set these are already removed from the availableSize before calling MeasureOverride. If the panel is hosted inside a ScrollViewer or other unlimited range (e.g. a Grid with an “Auto” size dimension) availableSize will have values of positive infinity for the appropriate dimensions.
MeasureOverride also returns a Size. This size may be used as the parameter passed into the ArrangeOverride method, depending on settings (discussed in more detail in Part 2). However, it’s important to note now that you cannot return a value of positive infinity for either dimension of the returned Size. A runtime System.InvalidOperationException will be thrown with message: MeasureOverride of element 'Clarity.Demo.CustomPanel.WrapPanel' should not return PositiveInfinity or NaN as its DesiredSize. You can avoid this by tracking the actual dimensions your child elements require or setting max values.
The true responsibility of the MeasureOverride method is to measure each UIElement child and use it’s DesiredSize property along with the child layout to determine the final size required for the panel (usually the Size that is then returned). Simply by calling UIElement.Measure(Size) on each child sets the DesiredSize property. Then you have to write your own logic to assess how the child fits into the chosen layout.
WrapPanel
For our panel the MeasureOverride method needs to measure each child element, determine if it fits to the right of the previous child, and wrap to the next line down if it does not. Throughout this process we will track our current height and width to know where the next child should be placed, as well as the maximum width reached. Sounds simple, right? Well, for our situation, it is.
protected override Size MeasureOverride(Size availableSize)
{
Size sizeSoFar = new Size(0, 0);
double maxWidth = 0.0;
foreach (UIElement child in Children)
{
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (sizeSoFar.Width + child.DesiredSize.Width > availableSize.Width)
{
sizeSoFar.Height += child.DesiredSize.Height;
sizeSoFar.Width = 0;
}
else
{
sizeSoFar.Width += child.DesiredSize.Width;
maxWidth = Math.Max(sizeSoFar.Width, maxWidth);
}
}
return new Size(maxWidth, sizeSoFar.Height);
}
Because we are creating a WrapPanel I allow each child as much space as it thinks it needs by passing an infinite Size into each child’s Measure method. If the child doesn’t fit on the current row I wrap it to the next row. Once we iterate through all the children we know the largest width of all the rows (maxWidth) and how far down we’ve wrapped (sizeSoFar.Height) – in other words, we know the total size the wrap panel requires – so we return this value.
Note that I’ve kept this simple by making two assumptions: 1) each child has the same height so we don’t have to track a maxHeight separately, and 2) that we will be able to fit at least one child per row.
Calling Measure
Great, so now we can Measure! But when does this happen? Firstly, the runtime will call this when attempting to render the panel. This happens anytime we add or remove children, show or hide, etc. But we can also force a call to MeasureOverride in different ways. One is to call InvalidateMeasure() on the FrameworkElement you’d like to force a Measure on. Another is a little more complex but useful. If your custom element has custom Dependency Properties the properties can be registered with metadata of FrameworkPropertyMetadataOptions.AffectsMeasure (WPF only). Then, when setting the property the control will already know to call Measure.
Next Steps
This gets us partway to creating a WrapPanel in Silverlight. Stay tuned for Part 2 where I discuss the ArrangeOverride and complete the simple example.
One of the most important concepts in UI design is the ability to organize your UI however you want. The framework panels all have their uses, but don’t allow the type of customization that is sometimes required. In order to create your own panels there are a couple of relatively simple steps that must be taken.
I will spend the next couple of posts looking at what it takes to make a custom Silverlight WrapPanel that is both animated and virtualized. However, before I get into the details, there are a couple of introductory concepts to address.
Firstly, I will be writing this code in Silverlight. Why Silverlight you ask? Isn’t WPF more full-featured you ask? Well, the short answer (at least from where I sit), is Yes. And that is exactly why I am writing this in Silverlight. Because if you can write it in Silverlight, you can write it in WPF, while the opposite isn’t necessarily true.
The next piece of info is that our custom panel will inherit from the base Panel class. This gives us access to the UIElementCollection Children. We could manage a child element collection ourselves, but by using the built-in Children property we don’t have to worry about explicitly adding and removing the UIElements from the visual and logical trees.
It is also necessary to know the order of operations for a rendering panel. A Panel will call into it’s MeasureCore method to determine how much room it needs to render. MeasureCore is sealed and so we can’t touch it, but MeasureOverride is exposed, allowing us to indirectly override the functionality of the MeasureCore method. After measuring, the panel’s ArrangeCore method is called. Again, this method is sealed, but ArrangeOverride is exposed for us to override. This method will actually place all child elements in a location relative to the parent panel. After arranging, the panel executes OnRender to actually “paint” the screen.
We will examine MeasureOverride and ArrangeOverride in more detail in subsequent posts, but nothing I have worked on has required overriding OnRender, so I’ll leave that one alone. Also note: calling Measure will always then call Arrange, which will then always call Render, but there is no guarantee that when Render is called Arrange was called previously, and likewise no guarantee that when Arrange is called Measure was also called.
The final, and potentially most complex, piece of this puzzle is virtualization. Making a panel “virtualizing” essentially means that we are only going to create controls for the elements on screen, while destroying any controls not currently visible. This gives a huge performance boost when working with long lists of items. Again, more detail to come.
It may have already become obvious to some of you, but we will go through the process of creating a custom animating and virtualizing panel in four discrete pieces:
Part 1: MeasureOverride
Part 2: ArrangeOverride
Part 3: Animation
Part 4: Virtualization
Stay tuned…
This topic has been covered by many people, but since I was recently looking at it myself, I thought I’d add it here.
My idea is to load resources at runtime to enable hot swapping of skins without having to recompile or restart the application. The basic idea is that you define a loose XAML file that contains all the resources you’d like to be swappable, then you load those resources at runtime. You can then change the XAML file at anytime and reload those resources.
In this example my window contains a simple uniform grid that uses dynamic resources to populate the fill color of rectangles.
<UniformGrid Rows="2" Columns="2">
<Rectangle Fill="{DynamicResource brush1}"/>
<Rectangle Fill="{DynamicResource brush2}"/>
<Rectangle Fill="{DynamicResource brush3}"/>
<Rectangle Fill="{DynamicResource brush4}"/>
</UniformGrid>
Note that you must use DYNAMIC resources for this to work. Otherwise the brush will be pulled from the App.xaml Resource block and will not update at runtime.
OK, so, now let’s add some resource files. Outside of the project I’ve created two resource dictionaries and named them “skin.xaml” and “skin_other.xaml.” These both hold simple SolidColorBrush definitions with different colors and are placed in the bin folder. They look like this:
skin:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="brush1" Color="Black"/>
<SolidColorBrush x:Key="brush2" Color="Gray"/>
<SolidColorBrush x:Key="brush3" Color="Brown"/>
<SolidColorBrush x:Key="brush4" Color="LightGray"/>
</ResourceDictionary>
skin_other:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="brush1" Color="Blue"/>
<SolidColorBrush x:Key="brush2" Color="Red"/>
<SolidColorBrush x:Key="brush3" Color="Green"/>
<SolidColorBrush x:Key="brush4" Color="Yellow"/>
</ResourceDictionary>
OK, so we’ve got the window ready to display our colors, and we’ve got our colors defined…all that’s left is to actually load and swap them. Inside App.xaml.cs I can load a resource dictionary with these two lines of code:
ResourceDictionary rd = (ResourceDictionary)XamlReader.Load(System.Xml.XmlReader.Create("skin.xaml"));
this.Resources.MergedDictionaries.Add(rd);
And that’s all it takes. Simple. Here though, to continue with my “hot swap” example, I’ve set up a timer which will switch between the two resource dictionaries every two seconds.
public partial class App : Application
{
DispatcherTimer timer = new DispatcherTimer();
public App()
{
timer.Interval = new TimeSpan(0, 0, 2);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
private bool flip = true;
void timer_Tick(object sender, EventArgs e)
{
ResourceDictionary rd;
if (flip)
{
rd = (ResourceDictionary)XamlReader.Load(System.Xml.XmlReader.Create("skin.xaml"));
}
else
{
rd = (ResourceDictionary)XamlReader.Load(System.Xml.XmlReader.Create("skin_other.xaml"));
}
this.Resources.MergedDictionaries.Add(rd);
flip = !flip;
}
}
And that is how you load resource dictionaries at runtime. Hope you enjoyed it.
Oh, one final note, I’ve added my newly loaded resource dictionary to the MergedDictionaries so that there is no impact on any other resources that might have been included in the application.
Select columns of text with Shift + Alt
If you ever find yourself copying and pasting a line of code to change one value in each line, this tip is for you. Let’s say you want to create 10 pieces of test data – to be aptly named test1, test2, and so on. So you write the first one, then copy and paste the line 9 times.
Now, of course, you have 9 lines to update. You can speed up the process by selecting the columns of “1”s. Place your cursor next to the character. Press Shift + Alt and then use your Left or Right Arrow keys to select the column to delete on your current line. Use the Down or Up Arrow keys and suddenly you’re selecting the column! It’s amazing!
This is just a little time saver, but I happen to use it often, and really enjoy it’s simplicity.
Just one little note, make sure you use a left or right arrow before you use the down or up arrow – otherwise VS assumes you want to select the entire line of code.

IntelliSense Help Keyboard Shortcut: Shift+Alt+F10
You know, you’re writing code with a Type you haven’t property included and you see that little bar pop up on the right.
Instead of reaching for the mouse, try hitting Shift+Alt+F10 and suddenly:
Wow! VS wants to help! Hit Enter to accept the suggestion and have VS add the appropriate using statement.
That’s it. I like this one, hope you do too.
Oh, um, let’s not talk about the fact that VS tells you this in the tool tip…

Last week I blogged about a generic Drag and Drop framework. That post focused on the framework and gave some simple implementations of it, but didn’t get into more practical examples. So, in honor of Opening Day, here’s a quick example of how to use the framework to create the Cincinnati Reds batting order by dragging from a list of players to another list.
Setup
The first thing we need to do is get all of our resources set up. I included the existing DragDropAdornerBase and DragDropHelper classes (discussed in the framework post) into my new project. Next, I set up the images of the Reds starting roster as application level resources.
Model
Now that our resources are in place, let’s make a quick object to represent a player. I named this class “Player” (surprise!). Player.cs has simple public properties to hold the data we’ll show on the screen: Picture, Name and Position.
namespace Clarity.Demo.ListDragDrop
{
public class Player
{
public string Name { get; set; }
public Position Position { get; set; }
public BitmapImage Img { get; set; }
public Player() { }
public Player(string name, Position position, BitmapImage img)
{
Name = name;
Position = position;
Img = img;
}
}
public enum Position
{
Pitcher = 1,
Catcher = 2,
First = 3,
Second = 4,
ShortStop = 5,
Third = 6,
Outfield = 7
}
}
View
We’ll also need a UserControl to show the player information. PlayerControl inherits from UserControl and has no code behind. In order to represent the player data visually without code behind PlayerControl takes advantage of DataBinding to the Player model object.
<Canvas Background="Red">
<Image x:Name="playerImage" Source="{Binding Img}" Margin="5 5 0 5" MaxHeight="90" MaxWidth="90"/>
<TextBlock x:Name="name" Text="{Binding Name}" Background="Transparent" Margin="100 15 5 0"/>
<TextBlock x:Name="position" Text="{Binding Position}" Background="Transparent" Margin="100 60 5 0"/>
</Canvas>
And now we have to build the window. Let’s make this as simple as possible, so we’ll just put 2 scrollable ItemsControls that use the PlayerControl as a DataTemplate.
<Window x:Class="Clarity.Demo.ListDragDrop.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Clarity.Demo.ListDragDrop"
Title="Window1" Height="600" Width="800" Loaded="Window_Loaded">
<Canvas>
<UniformGrid Rows="1" Columns="2" Height="550">
<ScrollViewer Margin="15 15 30 15" >
<ItemsControl x:Name="playerList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:PlayerControl Margin="5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ScrollViewer Grid.Column="1" Margin="30 15 15 15">
<ItemsControl x:Name="lineup">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:PlayerControl Margin="5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UniformGrid>
</Canvas>
</Window>
And now we’ll populate the playerList ItemsControl in the Window codebehind:
public partial class Window1 : Window
{
ObservableCollection<Player> _players = new ObservableCollection<Player>();
ObservableCollection<Player> _lineup = new ObservableCollection<Player>();
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
LoadPlayers();
playerList.ItemsSource = _players;
lineup.ItemsSource = _lineup;
}
private void LoadPlayers()
{
_players.Add(new Player("Aaron Harang", Position.Pitcher,
(BitmapImage)App.Current.TryFindResource("harang")));
_players.Add(new Player("Alex Gonzalez", Position.ShortStop,
(BitmapImage)App.Current.TryFindResource("gonzalez")));
_players.Add(new Player("Brandon Phillips", Position.Second,
(BitmapImage)App.Current.TryFindResource("phillips")));
_players.Add(new Player("Chris Dickerson", Position.Outfield,
(BitmapImage)App.Current.TryFindResource("dickerson")));
_players.Add(new Player("Edwin Encarnacion", Position.Third,
(BitmapImage)App.Current.TryFindResource("encarnacion")));
_players.Add(new Player("Jay Bruce", Position.Outfield,
(BitmapImage)App.Current.TryFindResource("bruce")));
_players.Add(new Player("Joey Votto", Position.First,
(BitmapImage)App.Current.TryFindResource("votto")));
_players.Add(new Player("Ramon Hernandez", Position.Catcher,
(BitmapImage)App.Current.TryFindResource("hernandez")));
_players.Add(new Player("Willy Taveras", Position.Outfield,
(BitmapImage)App.Current.TryFindResource("taveras")));
}
}
Now, running the app gives us the following window:
Implementation
OK, so at this point we’ve got the player model and view in place. Now, let’s add the fun stuff. First, let’s create an adorner for our drag and drop. Inheriting from the DragDropAdornerBase I’ve created a simple adorner that just shows the player picture (hiding the RenderTransforms, Animations and Triggers for simplicity).
<local:DragDropAdornerBase x:Class="Clarity.Demo.ListDragDrop.PlayerAdorner"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Clarity.Demo.ListDragDrop"
Height="75" Width="100">
<UserControl.RenderTransform...>
<UserControl.Resources...>
<UserControl.Triggers...>
<Grid x:Name="grid">
<Grid.RenderTransform...>
<Image x:Name="playerImage" Source="{Binding Img}" Margin="2"/>
</Grid>
</local:DragDropAdornerBase>
Now that adorner control is created, that means we’re ready to add it as a resource to Window1.
<Window.Resources>
<local:PlayerAdorner x:Key="adorner"/>
</Window.Resources>
Of course, there are three more pieces necessary to implement the drag and drop. 1) Window1 needs to contain an adorner layer, 2) the PlayerControl DataTemplates need to define the DragDropHelper attached properties to be recognized as Drag Sources, and 3) Window1 needs to subscribe to the DragDropHelper.ItemDropped event.
First the adorner layer gets added at the end of the Window1 xaml:
</UniformGrid>
<Canvas x:Name="adornLayer" Visibility="Collapsed"/>
</Canvas>
Next the playerList and lineup ItemsControls have their DataTemplates redefined:
<local:PlayerControl Margin="5"
local:DragDropHelper.AdornerLayer="adornLayer"
local:DragDropHelper.DragDropControl="{StaticResource adorner}"
local:DragDropHelper.DropTarget="lineup"
local:DragDropHelper.IsDragSource="true"/>
<local:PlayerControl Margin="5"
local:DragDropHelper.AdornerLayer="adornLayer"
local:DragDropHelper.DragDropControl="{StaticResource adorner}"
local:DragDropHelper.DropTarget="playerList"
local:DragDropHelper.IsDragSource="true"/>
And finally, Window1 subscribes to the ItemDropped event to add and remove the players accordingly.
void DragDropHelper_ItemDropped(object sender, DragDropEventArgs e)
{
Player p = e.Content as Player;
if (p == null) return;
if (_players.Contains(p))
{
_players.Remove(p);
_lineup.Add(p);
}
else if (_lineup.Contains(p))
{
_lineup.Remove(p);
_players.Add(p);
}
}
And that’s all it takes. Now we can drag and drop players from the left list to the right to set the batting order:
And back right to left if you change your mind

All source is available, “as-is” and without express or implicit warantee or support. Happy drag and dropping, and go Reds!
Next Steps
Stay tuned for Part 2, including some “hot-ification” of the dropping functionality.
Working with the illustrious design team at here at Clarity has opened my eyes about the user experience limitations of coding. Specifically, some of their concepts for a particular animation are great but just can’t be done (or at least not easily) with the built-in WPF animations.
And that is where the Image Sequencer comes in.
Originally used by a co-worker of mine, Erik Klimczak, to run the splash animation for the Clarity Endless Whiteboard, this simple WPF user control displays a sequence of images (.pngs here, but could be anything) and plays through them at just over 30 fps.
The xaml is extremely simple, just an Image to host the currently displaying frame:
<UserControl x:Class="Clarity.Demo.ImageSequencer.ImageSequencerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="Transparent">
<Grid>
<Image x:Name="image" Stretch="Fill"/>
</Grid>
</UserControl>
The real logic is the in the control’s code-behind. A simple timer is initialized to tick every 30 milliseconds.
public ImageSequencerControl()
{
InitializeComponent();
this.updateImageTimer = new DispatcherTimer(DispatcherPriority.Render);
this.updateImageTimer.Interval = TimeSpan.FromMilliseconds(30.0);
this.updateImageTimer.Tick += new EventHandler(this.updateImageTimer_Tick);
}
On every tick, the Image source is switched to the next image in the sequence.
private void updateImageTimer_Tick(object sender, EventArgs e)
{
if (this.currentIndex == this.images.Count)
{
this.currentIndex = 0;
}
if (this.images != null)
{
if (((this.images != null) && (this.currentIndex < this.images.Count)) && (this.currentIndex >= 0))
{
this.image.Source = this.images[this.currentIndex];
}
}
this.currentIndex++;
}
Basic Play() and Stop() methods start and stop the timer, and therefore the animation.
public void Play()
{
this.currentIndex = 0;
this.updateImageTimer.Start();
}
public void Stop()
{
this.updateImageTimer.Stop();
}
Typically animations less than about 12 fps start looking choppy and anything too much over 30 fps is overkill. I suggest playing around with the timer interval to see this for yourself.
The only thing left, now, is to add the sequencer to Window1 and load the images for the sequencer to flip through. The control is added in the window xaml:
<local:ImageSequencerControl x:Name="sequencer" Width="576" Height="384" />
Note that I’ve used a width and height here based on the sizing of the .pngs I’m animating. Those values should be set to scale your images appropriately.
I’m loading the images and starting the animation in the Window code-behind at when the Window_Loaded event fires. To use this yourself you’ll have to modify the “filename” string to match your image file naming conventions.
public void LoadImages()
{
_animationSeq = new List<BitmapSource>();
string str = ".Images.whiteboard";
string str2 = ".png";
Assembly executingAssembly = Assembly.GetExecutingAssembly();
for (int i = 0; i <= 300; i++)
{
string filename = string.Format("{0}{1}{2}{3}", this.GetType().Namespace, str, i.ToString("000"), str2);
BitmapImage item = new BitmapImage();
item.BeginInit();
item.StreamSource = executingAssembly.GetManifestResourceStream(filename);
item.CacheOption = BitmapCacheOption.OnLoad;
item.CreateOptions = BitmapCreateOptions.None;
item.EndInit();
item.Freeze();
_animationSeq.Add(item);
}
sequencer.Load(_animationSeq);
sequencer.Play();
}
And that’s all there is to it! Simple, clean, and looks great. Download the source to try this out and see all the code. And don’t forget to check out the link above!
As always, this is provided “as-is” without any express or implied warrantee or support.
I was recently working with a WPF application where we wanted drag and drop functionality. We were going to have multiple types of draggable items and different drop locations, and being the conscientious programmer that I am, I wanted to make something as generic as possible. I started from Bea Stollnitz’s post about drag and drop between ItemsControls and went from there.
All the source is available with the usual disclaimer that it comes “as-is” without any express or implied warrantee or support.
Concept
There are three main pieces for any drag and drop:
- Draggable Control – simply, the UIElement you’d like the user to be able to mouse-down and drag
- Drag Adorner – the control you are dragging around
- Droppable Area – a target area where the mouse-up (drop) will cause an action
I’ve made two base classes to handle the creation and definition of these three pieces and their intrinsic functionality. Note: these use some object oriented programming and WPF concepts (e.g. inheritance, dependency properties, attached properties) and if you’re not familiar with them you might want to do some quick background reading before continuing.
Base Functionality
The drag and drop functionality has to include some style, even if the controls themselves don’t (I’m just a humble programmer, folks, not a designer). So, on top of basic dragging and dropping I’ve added the following touches.
First, when you drag the object we are going to create a different view of the object to move around (the Drag Adorner). This is especially useful when the Draggable Control is large or contains a lot of text.
Second, we want the user to be conscious of the effect of their drop. So, we’ll use two animations: one to accept the object when dropped over the Droppable Area, and one to suck it back to the start point when dropped outside of the Droppable Area.
DragDropAdornerBase.cs
This is a class that inherits from UserControl without a .xaml file - allowing us to inherit our own Drag Adorner user control from it. The highlights of this file are:
- DropState – a simple enumeration where:
- CanDrop means the Drag Adorner is inside the Droppable Area
- CannotDrop means the Drag Adorner outside the Droppable Area
public enum DropState
{
CanDrop = 1,
CannotDrop = 2
}
- AdornerDropState – a dependency property to track the DropState of the object
public DropState AdornerDropState
{
get { return (DropState)GetValue(AdornerDropStateProperty); }
set { SetValue(AdornerDropStateProperty, value); }
}
public static readonly DependencyProperty AdornerDropStateProperty =
DependencyProperty.Register("AdornerDropState", typeof(DropState),
typeof(DragDropAdornerBase), new UIPropertyMetadata(DropStateChanged));
- StateChangedHandler – a virtual method called from the property changed callback to handle any state specific UI changes. This is used in one of the example implementations below.
public static void DropStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DragDropAdornerBase myclass = (DragDropAdornerBase)d;
myclass.StateChangedHandler(d,e);
}
public virtual void StateChangedHandler(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
DragDropHelper.cs
This class defines the attached properties used to declare the Draggable Control, Drag Adorner and Droppable Area and registers listeners to control the functionality of the drag and drop.
- IsDragSource – a simple boolean which, set to True, defines a Draggable Control. The property changed callback, IsDragSourceChanged, wires up the required event listeners
public static readonly DependencyProperty IsDragSourceProperty =
DependencyProperty.RegisterAttached("IsDragSource", typeof(bool),
typeof(DragDropHelper), new UIPropertyMetadata(false, IsDragSourceChanged));
private static void IsDragSourceChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var dragSource = obj as UIElement;
if (dragSource != null)
{
if (Object.Equals(e.NewValue, true))
{
dragSource.PreviewMouseLeftButtonDown += Instance.DragSource_PreviewMouseLeftButtonDown;
dragSource.PreviewMouseLeftButtonUp += Instance.DragSource_PreviewMouseLeftButtonUp;
dragSource.PreviewMouseMove += Instance.DragSource_PreviewMouseMove;
}
else
{
dragSource.PreviewMouseLeftButtonDown -= Instance.DragSource_PreviewMouseLeftButtonDown;
dragSource.PreviewMouseLeftButtonUp -= Instance.DragSource_PreviewMouseLeftButtonUp;
dragSource.PreviewMouseMove -= Instance.DragSource_PreviewMouseMove;
}
}
}
- DragDropControl – a UIElement that defines the Drag Adorner control
- AdornerLayer – a Canvas object for containing and displaying the Drag Adorner control
public static readonly DependencyProperty DragDropControlProperty =
DependencyProperty.RegisterAttached("DragDropControl", typeof(UIElement),
typeof(DragDropHelper), new UIPropertyMetadata(null));
public static readonly DependencyProperty AdornerLayerProperty =
DependencyProperty.RegisterAttached("AdornerLayer", typeof(string),
typeof(DragDropHelper), new UIPropertyMetadata(null));
- DropTarget – the string name of the UIElement that defines the Droppable Area
public static readonly DependencyProperty DropTargetProperty =
DependencyProperty.RegisterAttached("DropTarget", typeof(string),
typeof(DragDropHelper), new UIPropertyMetadata(string.Empty));
Now that we’ve know how to define the necessary pieces, let’s look at how the event handlers define the functionality.
- DragSource_PreviewMouseLeftButtonDown – sets DragDropHelper instance variables
- DragSource_PreviewMouseLeftButtonUp – clears DragDropHelper instance variables. This method is only called if the mouse button is released prior to a recognized drag
- DragSource_PreviewMouseMove – determines if the mouse move is large enough to be considered a drag. If a drag is detected this method shows the AdornerLayer Canvas, adds the defined Drag Adorner (DragDropControl) to the Canvas, and wires-up listeners to mouse move and mouse up actions on the Drag Adorner
private void DragSource_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (!_mouseCaptured && _draggedData != null)
{
// Only drag when user moved the mouse by a reasonable amount.
if (DragDropHelper.IsMovementBigEnough(_initialMousePosition, e.GetPosition(_topWindow)))
{
_adorner = (DragDropAdornerBase)GetDragDropControl(sender as DependencyObject);
_adorner.DataContext = _draggedData;
_adorner.Opacity = 0.7;
_adornerLayer.Visibility = Visibility.Visible;
_adornerLayer.Children.Add(_adorner);
_mouseCaptured = Mouse.Capture(_adorner);
Canvas.SetLeft(_adorner, _initialMousePosition.X - 20);
Canvas.SetTop(_adorner, _initialMousePosition.Y - 15);
_adornerLayer.PreviewMouseMove+=new MouseEventHandler(_adorner_MouseMove);
_adornerLayer.PreviewMouseUp += new MouseButtonEventHandler(_adorner_MouseUp);
}
}
}
- _adorner_MouseMove – tracks the mouse movement during a drag to reposition the Drag Adorner on the AdornerLayer and updates the Drag Adorner’s AdornerDropState
private void _adorner_MouseMove(object sender, MouseEventArgs e)
{
Point currentPoint = e.GetPosition(_topWindow);
currentPoint.X = currentPoint.X - 20;
currentPoint.Y = currentPoint.Y - 15;
_delta = new Point(_initialMousePosition.X - currentPoint.X,
_initialMousePosition.Y - currentPoint.Y);
_scrollTarget = new Point(_initialMousePosition.X - _delta.X,
_initialMousePosition.Y - _delta.Y);
Canvas.SetLeft(_adorner, _scrollTarget.X);
Canvas.SetTop(_adorner, _scrollTarget.Y);
_adorner.AdornerDropState = DropState.CannotDrop;
if (_dropTarget != null)
{
GeneralTransform t = _dropTarget.TransformToVisual(_adornerLayer);
_dropBoundingBox = t.TransformBounds(
new Rect(0, 0, _dropTarget.RenderSize.Width, _dropTarget.RenderSize.Height));
if (e.GetPosition(_adornerLayer).X > _dropBoundingBox.Left &&
e.GetPosition(_adornerLayer).X < _dropBoundingBox.Right &&
e.GetPosition(_adornerLayer).Y > _dropBoundingBox.Top &&
e.GetPosition(_adornerLayer).Y < _dropBoundingBox.Bottom)
{
_adorner.AdornerDropState = DropState.CanDrop;
}
}
}
- _adorner_MouseUp – animates the Drag Adorner based on the AdornerDropState and cleans up the resources used for dragging. Note that this code assumes the Drag Adorner User Control defines two animations in it’s resources named “canDrop” and “cannotDrop”, and that further assumptions are made about the animation itself when the item is dropped outside the Droppable Area.
private void _adorner_MouseUp(object sender, MouseEventArgs e)
{
switch (_adorner.AdornerDropState)
{
case DropState.CanDrop:
try
{
((Storyboard)_adorner.Resources["canDrop"]).Completed += (s, args) =>
{
_adornerLayer.Children.Clear();
_adornerLayer.Visibility = Visibility.Collapsed;
};
((Storyboard)_adorner.Resources["canDrop"]).Begin(_adorner);
if (ItemDropped != null)
ItemDropped(_adorner, new DragDropEventArgs(_draggedData));
}
catch (Exception ex)
{ }
break;
case DropState.CannotDrop:
try
{
Storyboard sb = _adorner.Resources["cannotDrop"] as Storyboard;
DoubleAnimation aniX = sb.Children[0] as DoubleAnimation;
aniX.To = _delta.X;
DoubleAnimation aniY = sb.Children[1] as DoubleAnimation;
aniY.To = _delta.Y;
sb.Completed += (s, args) =>
{
_adornerLayer.Children.Clear();
_adornerLayer.Visibility = Visibility.Collapsed;
};
sb.Begin(_adorner);
}
catch (Exception ex) { }
break;
}
Implementation
Now that the generic functionality is all set up we’re at the fun part: using it for specific implementations.
I’ve created two examples in a single window for these. The first example is dragging an image, and the second drags text. Both draggable controls have separate drop areas defined (in case you forgot, I’m not a designer).
Leveraging the generic code to create specific instances takes only a few short steps. The first step is creating a child class of the DragDropAdornerBase, defining the look and feel of the adorner. This should define appropriate animations as well as override the StateChangedHandler for DropState as necessary for specific UI changes.
Once the adorner control is defined, the attached properties have to be set on a UIElement to enable dragging and to define the adorner object, the adorner layer and the drop area.
Example1 – ImageDragDropAdorner
This example will enable us to drag and drop the album cover into the yellow drop area.
First, an override of the DragDropAdornerBase class called the ImageDragDropAdorner is created to define the look of the Drag Adorner. For this example we’ll simply show the album cover and the name of the album image.
<local:DragDropAdornerBase x:Class="Clarity.Demo.GenericDragDrop.Example1.ImageDragDropAdorner"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Clarity.Demo.GenericDragDrop"
Height="100" Width="100">
<UserControl.Resources>
<Storyboard x:Key="canDrop"...>
<Storyboard x:Key="cannotDrop"...>
<Storyboard x:Key="loadAni"...>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=".8*"/>
<RowDefinition Height=".2*"/>
</Grid.RowDefinitions>
<Image Source="{Binding ImageSource}"/>
<TextBlock Grid.Row="1" Text="{Binding ImageName}" HorizontalAlignment="Center"/>
</Grid>
</local:DragDropAdornerBase>
The three collapsed Storyboards define animations for the adorner load, as well as dropped animations for both DropStates. There is nothing in the code behind for this control. The second step is set all the DragDropHelper attached properties in the Window1.xaml.
<Window x:Class="Clarity.Demo.GenericDragDrop.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Clarity.Demo.GenericDragDrop"
xmlns:ex1="clr-namespace:Clarity.Demo.GenericDragDrop.Example1"
xmlns:ex2="clr-namespace:Clarity.Demo.GenericDragDrop.Example2"
Title="Window1" Height="400" Width="500">
<Window.Resources>
<ex1:ImageDragDropAdorner x:Key="imageAdorner"/>
<ex2:TextDragDropAdorner x:Key="ddAdorner"/>
</Window.Resources>
<Canvas>
<TextBlock x:Name="dragSource" Text="Drag Me!" Background="CornflowerBlue" DataContext="Drag Me!" Margin="10" MaxHeight="50" MaxWidth="50"
local:DragDropHelper.IsDragSource="true"
local:DragDropHelper.DragDropControl="{StaticResource ddAdorner}"
local:DragDropHelper.DropTarget="dropTarget"
local:DragDropHelper.AdornerLayer="adornerLayer"/>
<Ellipse x:Name="dropTarget" Margin="200 200 0 0" Width="200" Height="100" Fill="LightGreen"/>
<TextBlock Margin="250 240 0 0" Text="Drop Text Here"/>
<Image Margin="10 100 10 0" Source="{Binding ImageSource}" Width="120" Height="120"
local:DragDropHelper.IsDragSource="true"
local:DragDropHelper.DragDropControl="{StaticResource imageAdorner}"
local:DragDropHelper.DropTarget="imageDropTarget"
local:DragDropHelper.AdornerLayer="adornerLayer"/>
<TextBlock x:Name="imageDropTarget" Background="Yellow" Text="Drop Album Here" Margin="200 10 0 0" Width="200" Height="150" />
<Canvas x:Name="adornerLayer" Visibility="Collapsed"/>
</Canvas>
</Window>
Let’s look at this XAML piece by piece. First, the Image and following TextBlock:
<Image Margin="10 100 10 0" Source="{Binding ImageSource}" Width="120" Height="120"
local:DragDropHelper.IsDragSource="true"
local:DragDropHelper.DragDropControl="{StaticResource imageAdorner}"
local:DragDropHelper.DropTarget="imageDropTarget"
local:DragDropHelper.AdornerLayer="adornerLayer"/>
<TextBlock x:Name="imageDropTarget" Margin="200 10 0 0" Width="200" Height="150"
Background="Yellow" Text="Drop Album Here"/>
The first attached property sets this Image draggable. Remember, the DragDropControl property is defined as a UIElement, and here that UIElement is defined in the Window’s Resources. The DragDropControl property is set second using a StaticResource with the key “imageAdorner.”
<Window.Resources>
<ex1:ImageDragDropAdorner x:Key="imageAdorner"/>
<ex2:TextDragDropAdorner x:Key="ddAdorner"/>
</Window.Resources>
The third property defines the Droppable Area by setting the DropTarget to the name of the target. In this example, that target is the TextBlock named “imageDropTarget.” And finally, the Adorner Layer is defined by setting the property to the name of the Canvas added at the bottom of Window1.xaml.
And that is all it takes! A simple user control and four attached property definitions and now we can drag and drop our album cover. I encourage you to download the source and run it, but it looks a little something like this:
Well, that was easy, but there’s not indication when we are over the drop target. That’s because the StateChangedHandler was not overridden in the adorner implementation. Let’s try that next.
Example2 - TextDragDropAdorner
For this example I incorporated some visual feedback for the user as to the Drag Adorner’s DropState, both a border and an icon will indicate to the user the drop state.
<local:DragDropAdornerBase x:Class="Clarity.Demo.GenericDragDrop.Example2.TextDragDropAdorner"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Clarity.Demo.GenericDragDrop"
MinWidth="80" MinHeight="30"
>
<UserControl.RenderTransform...>
<UserControl.Resources...>
<UserControl.Triggers...>
<Grid x:Name="grid" RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Grid.RenderTransform>
<Rectangle x:Name="back" StrokeThickness="2" RadiusX="8" RadiusY="8" Fill="#7F000000" Stroke="LightGray" />
<ContentControl x:Name="content" Content="{Binding}" HorizontalAlignment="Center" VerticalAlignment="Center" TextBlock.FontFamily="Myriad Pro" TextBlock.FontSize="12" TextBlock.Foreground="#dcdcdc" Foreground="#FFFFFFFF" />
<Image Opacity=".8" x:Name="indicator" Width="20" Height="20" Stretch="Uniform" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,-12,-12,0" />
</Grid>
</local:DragDropAdornerBase>
I’ve decided to use the Rectangle “back” Stroke property to define our border and the Image “indicator” as the icon. But this XAML doesn’t know anything about the AdornerDropState. I could use DataBinding and ValueConverters, but for the purposes of this example I didn’t.
Instead, the DragDropAdornerBase class’ virtual StateChangedHandler method is overridden in the child class code behind to provide the functionality we need.
public override void StateChangedHandler(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextDragDropAdorner myclass = (TextDragDropAdorner)d;
switch ((DropState)e.NewValue)
{
case DropState.CanDrop:
myclass.back.Stroke = Application.Current.Resources["canDropBrush"] as SolidColorBrush;
myclass.indicator.Source = Application.Current.Resources["dropIcon"] as DrawingImage;
break;
case DropState.CannotDrop:
myclass.back.Stroke = Application.Current.Resources["solidRed"] as SolidColorBrush;
myclass.indicator.Source = Application.Current.Resources["noDropIcon"] as DrawingImage;
break;
}
}
Of course, the various brushes and icons are defined in the application resource dictionary.
Now we set the appropriate attached properties in Window1 as in the previous example (using a different drag adorner resource and a different drop target):
<TextBlock Margin="10" x:Name="dragSource" Text="Drag Me!" DataContext="Drag Me!" Background="Blue" MaxHeight="50" MaxWidth="50"
local:DragDropHelper.IsDragSource="true"
local:DragDropHelper.DragDropControl="{StaticResource ddAdorner}"
local:DragDropHelper.DropTarget="dropTarget"
local:DragDropHelper.AdornerLayer="adornerLayer"/>
That’s all the setup. Try it out by clicking and dragging:
Now drag over the target area:
Notice that the blue border is visible and the red ‘x’ is now a yellow ‘+’? It works!
Practical Use
The code above is all we need to use generic base classes to create drag and droppable objects, which is cool, but what’s the point? The point is to manipulate the data behind the draggable object. Maybe you want to add the album to a play list, or you want to filter a list based on a word in a tag cloud, or maybe you want to trash a file - what ever the purpose, these are practical uses of drag and drop.
To support this there are two more pieces of the DragDropHelper class that we need. The first is a way to move data, and the second is a way to announce that data was moved. I’ve done this by defining an object in the helper class called “_draggedData” which is set in the DragSource_PreviewMouseLeftButtonDown method.
_draggedData = (sender as FrameworkElement).DataContext;
Now that the data is saved, we just have to broadcast that it was dropped. So, in the _adorner_MouseUp method, if the Drag Adorner is in the CanDrop DropState:
if (ItemDropped != null)
ItemDropped(_adorner, new DragDropEventArgs(_draggedData));
Where the ItemDropped event and DragDropEventArgs are defined as:
public static event EventHandler<DragDropEventArgs> ItemDropped;
public class DragDropEventArgs : EventArgs
{
public object Content;
public DragDropEventArgs() { }
public DragDropEventArgs(object content)
{
Content = content;
}
}
OK, great, we’ve got the data and we’re broadcasting, but so what? No classes are holding a DragDropHelper instance and listening for the event. Except, because the event is static ANY class can register a listener without an instance. One line is all it takes to wire up the listener.
DragDropHelper.ItemDropped += new EventHandler<DragDropEventArgs>(DragDropHelper_ItemDropped);
Conclusion
With some up front work in the base classes it is now a simple matter create new implementations of a WPF drag and drop control.
After a short stay and a significant hiatus, I am attempting a return to the blogging world. I expect to focus the content mostly on WPF, C# and VS2008, but who knows what other tidbits could make it in. Wish me luck!
It's finally here! What you've all been waiting for! OK, so no one has been waiting for it...but if you are interested I created a short (3 pictures), simple version of PhotoHunt for the Pocket PC. It should run on any Pocket PC running Windows Mobile 5.0 and .NET CF 2.0. Download the zip
here. Then just use ActiveSync to put the unzipped files (there are 2) on your PPC. Click Photohunt.exe to start and your playing. Enjoy.
Recently I've been playing with creating an application for a Pocket PC. Visual Studio 2005 (with Pocket PC SDK) and ActiveSync 4.1 make this a relatively simple project. Simple means that VS2005 provides a form designer and emulator for all your design and debugging needs. Once you've connected your actual device with ActiveSync you can deploy to it with just a quick change of menu option (If you want more details on this process I can write up a quick guide).
So, what could possibly go wrong? Well, you your device should have the latest version of the .NET Compact Framework. Fortunately VS2005 offers this nifty little option in the solution properties, devices tab: "Deploy the latest version of the .NET Compact Framework". Just make sure that option is checked, right? Except it just wasn't that simple. After a few attempts failed and I was beginning to get frustrated I decided to forego the built-in option and update the framework myself. I uninstalled the framework and reinstalled it on the device. And, surprise! now it works!...no more deployment errors, just fun fun fun!
I don't know why the VS2005 option resulted in error after error, but I am glad that Microsoft's installer packet worked.
Wikipedia announced their one millionth English article, and claims to have 3.3 million articles in more than 125 languages. While I am an avid user of the site (as well as everyone else I know) sometimes I wonder just how accurate all the facts are. Sure, articles are labeled with "Under Review" and other disclaimers when they are caught, but how many can they catch? Assuming 5% of articles are "tainted" and only 5% of those slip through we are still talking about over 8,000 erroneous articles. Are we putting too much faith in the masses here? Are we assuming that the informed minority will be able to control the overwhelming majority who either don't know what they are talking about, or intentionally post misinformation? I'll continue to use the site and others like it, and I assume you will too, but be aware...that's all I'm saying.
More Posts
Next page »