June 2010

Volume 25 Number 06

UI Frontiers - The Ins and Outs of ItemsControl

By Charles Petzold | June 2010

Charles PetzoldIf someone were to ask me what single class most epitomizes the power and flexibility of Windows Presentation Foundation (WPF) and Silverlight, I’d first say that it’s a stupid question and then, without a moment’s hesitation, respond “DataTemplate.”

A DataTemplate is basically a visual tree of elements and controls. Programmers use DataTemplates to give a visual appearance to non-visual data objects. Properties of elements in the visual tree are linked to properties of the data objects through bindings. Although the DataTemplate is most commonly used to define the appearance of objects in an ItemsControl or a ListBox (one of the classes that derive from ItemsControl), you can also use a DataTemplate to define the appearance of an object set to the Content property of a ContentControl or a ContentControl derivative, such as a button.

Creating a DataTemplate—or any other type of FrameworkTemplate derivative such as ControlTemplate or HierarchicalDataTemplate—is one of the few Silverlight programming tasks that can’t be done in code. You need to use XAML. It was once possible to create WPF templates entirely in code using Framework-ElementFactory, but I think I was the only person to actually publish examples (in chapters 11, 13 and 16 of my book, “Applications = Code + Markup” [Microsoft Press, 2006]) and the technique has now been deprecated.

What I want to show you in this article is a variation of drag-and-drop: The user simply moves an item from one ItemsControl to another. But my major objective is to implement this whole process with an entirely fluid look and feel that seems natural, and where nothing suddenly jerks or disappears. Of course, “the natural look” is often painstakingly achieved, and any program that strives for fluidity needs to avoid revealing all the awkward machinations just underneath the surface.

I’ll be using a combination of techniques I showed in last month’s column (“Thinking Outside the Grid,”) as well a DataTemplate that’s shared among two ItemsControls and a ContentControl—a concept essential to this whole program.

Program Layout

The downloadable code that accompanies this article contains a single Silverlight project named ItemsControlTransitions, which you can run from my Web site at charlespetzold.com/silverlight/ItemsControlTransitions2. (I’ll explain the “2” at the end of this URL later.) You can use the same concepts presented in this Silverlight program in a WPF program.

The program displays two Items Controls, both contained in ScrollViewers. You can visualize the ItemsControl at the left as a “market” selling produce. The one at the right is your “basket.” You use the mouse to pick produce items from the market and move them into the basket. Figure 1 shows the Corn item in transition from market to basket.

Figure 1 The ItemsControlTransitions Display
Figure 1 The ItemsControlTransitions Display

Although the Corn item has been moved out of the market, notice the gap in the ItemsControl that continues to indicate the source of the item. If the user releases the mouse button before the dragged item has been positioned over the basket Items-Control, the program animates the item back into the market. Only when the item is dropped into the basket does that gap close, again with an animation. Depending on where the item is dropped, an animated gap opens to receive the item, and the item is animated into position.

Once an item has been moved from the market, it no longer exists there, but that’s a program detail that could easily be changed. No facility exists to remove an item from the basket and move it back into the market, but that feature or something similar could be added fairly easily as well.

Figure 2 shows the bulk of the XAML file responsible for the basic layout. (Missing are two storyboards with seven animations that I’ll describe later.)

Figure 2 Partial XAML File Responsible for Basic Layout

<UserControl x:Class="ItemsControlTransitions.MainPage"   
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  Name="this">
    <UserControl.Resources>
      <DataTemplate x:Key="produceDataTemplate">
        <Border Width="144"
          Height="144"
          BorderBrush="Black"
          BorderThickness="1"
          Background="AliceBlue"
          Margin="6">
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="*" />
              <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <Image Grid.Row="0"
              Source="{Binding Photo}" />
            <TextBlock Grid.Row="1"
              Text="{Binding Name}"
              HorizontalAlignment="Center" />
          </Grid>
        </Border>
      </DataTemplate>

        ...
        
    </UserControl.Resources>

    <Grid x:Name="LayoutRoot" Background="White">
        <ScrollViewer HorizontalAlignment="Left"
          Margin="48">
            <ItemsControl Name="market"
              ItemTemplate="{StaticResource produceDataTemplate}"
                Width="156"
                MouseLeftButtonDown="OnMarketItemsControlMouseLeftButtonDown" />
        </ScrollViewer>

        <ScrollViewer HorizontalAlignment="Right"
          Margin="48">
            <ItemsControl Name="basket"
              ItemTemplate="{StaticResource produceDataTemplate}"
            Width="156" />
        </ScrollViewer>

        <Canvas Name="dragCanvas">
          <ContentControl Name="dragControl"
            ContentTemplate="{StaticResource produceDataTemplate}"
            Visibility="Collapsed" />
        </Canvas>
    </Grid>
</UserControl>

The Resources section contains a DataTemplate for displaying the produce items, and a reference to this resource is set to the ItemsTemplate property of the two ItemsControls.

In addition, a Canvas covers the entire area occupied by the program. You’ll recall from last month’s column how you can use a Canvas to host items that need to “float” over the rest of the UI. The only child of this Canvas is a ContentControl, with its ContentTemplate also set to that DataTemplate. But the Visibility property is set to Collapsed so this ContentControl is initially invisible.

Controls that derive from ContentControl are common in WPF and Silverlight applications, but it’s not often you see a  ContentControl itself. It turns out to be extremely handy if all you want is to display an object using a DataTemplate. Visually, it’s very much like a single item in an ItemsControl.

The program begins by loading in a little XML database of some produce—using the same files from the ItemsControlPopouts project in last month’s column—and then filling up the market ItemsControl with objects of type ProduceItem. This class has Name and Photo properties that the DataTemplate references to display each item.

Pulling Items from ItemsControl

The ItemsControl for the market has a handler set for MouseLeftButtonDown. On receipt of this event, the program needs to dislodge an item from the four-wall confines of the ItemsControl and allow it to track the mouse. But the item can’t actually be removed from the ItemsControl or the gap will automatically close up.

As I demonstrated in last month’s column, you can access the ItemContainerGenerator property of an ItemsControl to get a class that can associate each item in an ItemsControl with the visual tree that’s been generated to display that particular item. This visual tree has a root element of type ContentPresenter.

My first impulse was to apply a TranslateTransform to the RenderTransform property of the ContentPresenter, to allow it to float outside the ItemsControl. I know from experience, however, that this doesn’t work at all. The problem isn’t the ItemsControl itself; the problem is the ScrollViewer, which of necessity clips its children to its interior. (More about the rationale behind this clipping shortly.)

Instead, the program copies the clicked ProduceItem in the ItemsControl to the ContentControl, and positions the ContentControl precisely over the ContentPresenter of the clicked item. (The program can obtain the location of the ContentPresenter relative to the Canvas using the always handy TransformToVisual method.) You’ll recall that the XAML file sets the Visibility property of the ContentControl to Collapsed, but now the program toggles that property to Visible.

At the same time, the ContentPresenter in the ItemsControl is made invisible. In WPF, you can do this simply by setting the Visibility property to Hidden, which makes the item invisible but otherwise causes the element’s size to be observed for layout purposes. The Visibility property in Silverlight doesn’t have a Hidden option, and if you set the Visibility property of the ContentPresenter to Collapsed, the gap will close up. Instead, you can mimic a Visibility setting of Hidden by simply setting the Opacity property to zero. The element is still there, but it’s invisible. As you experiment with the program, you’ll discover that the transition from the item in the ItemsControl to the draggable ContentControl is imperceptible.

At this point, the ContentPresenter in the ItemsControl displays nothing but an empty hole, and the ContentControl displaying the item can now be dragged around the screen with the mouse.

The Item Drop

Back when I was writing books about the Win16 and Win32 APIs, I spent whole chapters showing how to use scroll bars to display more text in a window than can fit there. Today we simply use a ScrollViewer, and everyone is much happier—me most of all.

Despite its essential role in WPF and Silverlight layout, the ScrollViewer can be a little tricky to use at times.  It has some peculiarities that can be a little puzzling, and this program reveals one of them. See if you can anticipate the problem.

We left the user moving a produce item around the screen with the mouse. If the user drops the produce item somewhere over the ItemsControl representing the basket, it becomes part of that collection. (More on this process shortly.) Otherwise, the program animates the item back to its origin using two animations in the returnToOriginStoryboard in MainPage.xaml. At the conclusion of the animation, the Opacity property of the ContentPresenter is set to one, the Visibility property of the ContentControl is set to Collapsed, and the drag event is concluded with everything back to normal.

To determine if the produce item is being dropped on the ItemsControl, the program calculates a Rect object representing the location and size of the dragged ContentControl and another Rect object representing the location and size of the ItemsControl. In both cases, the program uses the TransformToVisual method to obtain the location of the upper-left corner of the control—the point (0, 0)—relative to the page, and the ActualWidth and ActualHeight properties to obtain the control’s size. The Rect structure’s Intersect method then calculates an intersection of the two rectangles, which will be non-empty if there’s some overlap.

This works fine except when the ItemsControl has more items than can fit in the vertical space allowed for it. The ScrollViewer then kicks into action by making its vertical scrollbar visible so you can scroll through the items. However, the ItemsControl inside the ScrollViewer actually believes itself to be larger than what you’re seeing; in a very real sense, the ScrollViewer is providing only a viewable window (called a “viewport”) on the ItemsControl. The location and size information you obtain for that ItemsControl always indicates the full size (called the “extent” size) and not the viewport size. 

This is why ScrollViewer needs to clip its child. If you’ve been working with Silverlight for a while, you might be particularly accustomed to a certain laxity regarding clipping of children. You can almost always use RenderTransform to escape from a parent’s boundaries. However, ScrollViewer definitely needs to clip or it simply can’t work right.

This means that you can’t use the apparent dimensions of the ItemsControl to determine a valid drop, because in some cases the ItemsControl extends above and below the ScrollViewer. For that reason, my program determines a valid drop rectangle based on horizontal dimensions of the ItemsControl—because it wants to exclude the area occupied by the scrollbar—but the vertical dimensions of the ScrollViewer.

When the ContentControl is dropped on the ItemsControl, it could be overlapping two existing items, or just one if it’s being dropped on the top or bottom of the stack of items, or none at all. I wanted to insert the new item in the spot closest to where it’s dropped, which required enumerating through the items in the ItemsControl (and their associated ContentPresenter objects) and determining a good index to insert the new item. (The GetBasketDestinationIndex method is responsible for determining this index.) After the item is inserted, the ContentPresenter associated with that new item is given an initial height of zero and an opacity of zero, so it isn’t initially visible.

Following this insertion, the program initiates the storyboard called transferToBasketStoryboard with five animations: one to decrease the height of the invisible ContentPresenter in the ItemsControl for the market; another to increase the height of the invisible ContentPresenter newly created in the basket ItemsControl; and two more to animate the Canvas.Left and Canvas.Top attached properties to slide the ContentControl into place. (I’ll discuss the fifth animation shortly.) Figure 3 shows the gap widening as the ContentControl approaches its destination.

Figure 3 The Animation to Move a New Item into Place
Figure 3 The Animation to Move a New Item into Place

When the animation ends, the new ContentPresenter is given an opacity of one and the ContentControl is given a visibility of Collapsed, and now we’re back to just dealing with two normal ItemsControls inside ScrollViewers.

The Top and Bottom Problem

Earlier in this article I gave you the URL charlespetzold.com/silverlight/ItemsControlTransitions2 to try out the program. An earlier version of the program can be run from charlespetzold.com/silverlight/ItemsControlTransitions, without the “2” on the end. Using this earlier version, move several produce items over to the basket—enough to make the vertical scrollbar appear. Now drag another one over and position it straddling the bottom of the ScrollViewer. When you release the mouse button, the ContentControl moves down toward an area of the ItemsControl that’s invisible, and then suddenly disappears. The item has been correctly inserted (as you can verify by scrolling down), but not very elegantly.

Now scroll the ScrollViewer so the top item is only partially visible.  Move another item from the basket and position it so it will be inserted before that item. The new item slides into the ItemsControl, but it’s not entirely visible. It’s not quite as bad as the problem at the bottom of the ItemsControl, but it still needs some help.

The fix? Some way to programmatically scroll the ScrollViewer is required. The amount of vertical scrolling currently in effect for a ScrollViewer is provided through the VerticalOffset property. This number is a positive offset from the top of the entire ItemsControl to the location in the control that’s displayed at the top of the ScrollViewer.

Wouldn’t it be nice to simply animate that VerticalOffset property? Unfortunately, only the get accessor is public. Fortunately, it’s possible to programmatically scroll the ScrollViewer, but you need to call a method named ScrollToVerticalOffset.

To accomplish this little scrolling job through the Silverlight animation facility, I defined a dependency property named Scroll in MainPage itself. In the XAML file, I gave the page a name of “this,” and defined a fifth animation in transferToBasketStoryboard to target this property:

<DoubleAnimation x:Name="scrollItemsControlAnima"
                 Storyboard.TargetName="this"
                 Storyboard.TargetProperty="Scroll" />

The OnMouseLeftButtonUp override calculates the From and To values of this animation.  (You can compare the effect of this additional animation by commenting out the block of code beginning with the comment “Calculate ScrollViewer scrolling animation.”)  As this Scroll property is animated, its property-changed handler calls the ScrollToVerticalOffset method of the ScrollViewer with the animated value.

Toward a Fluid UI

Many, many years ago, computers were much slower than they are now, and nothing that happened on the screen was ever very startling. Today, programs can implement UIs that entirely change their appearances in the blink of an eye. But that’s unsatisfactory as well. Often we can’t even see what’s going on, so now we find it necessary to deliberately slow down the UI and make transitions more fluid and natural. Silverlight 4 introduced some “fluid UI” features that I’m eager to discuss, but even in Silverlight 3 it’s possible to begin the journey in that direction.    


Charles Petzold is a longtime contributing editor to MSDN Magazine*. He is currently writing “Programming Windows Phone 7 Series,” which will be published as a free downloadable e-book in the fall of 2010. A preview edition is currently available through his Web site charlespetzold.com.*