Blog

Books

Books I tech reviewed and recommend
Sorry I can't reply to every email and comment. If you need a fast response, try the WPF Forum or Silverlight Forum.
November 16, 2009

Silverlight’s CollectionViewSource

In my last post, I explained the reasoning behind adding CollectionViewSource to WPF. In this post I will talk about CollectionViewSource in Silverlight (introduced in Silverlight 3) and compare it with its WPF counterpart.

If you recall from my last post, CollectionViewSource was added to WPF mainly to permit view-related scenarios to be expressed in XAML (so they could be tooled), and also to aid in remembering the current item of previously displayed collections. The reasons for adding CollectionViewSource to Silverlight were very different. Before the introduction of CollectionViewSource, Silverlight collections were not automatically wrapped by views – in fact the concept of “views” didn’t even exist in Silverlight 2. There was no way to sort or filter a collection (other than modifying the collection itself), or to build a master-detail scenario based on currency (although you could create a master-detail scenario based on selection). The introduction of CollectionViewSource enabled all of those scenarios in Silverlight, while improving compatibility with WPF.

Just like in WPF, a class is considered a “view” if it implements ICollectionView. All the views that we’re used to interacting with derive from CollectionView, which in turn implements ICollectionView. Silverlight provides implementations only for EnumerableCollectionView and ListCollectionView, which means that it is able to generate views only for collections that implement IEnumerable or IList. These are by far the two most common scenarios. Unlike WPF, Silverlight’s CollectionView, EnumerableCollectionView and ListCollectionView classes are all internal.

In addition to these, Silverlight also contains a PagedCollectionView class (this is unique to Silverlight). You can manually wrap your collection with this view to add filtering, sorting, grouping, currency tracking and paging to your collection. Tim Heuer shows an example of its usage. Silverlight’s CollectionViewSource, on the other hand, provides the ability to filter, sort, and track currency, but it does not offer the ability to group data.

Unlike WPF, Silverlight only wraps collections with a view when CollectionViewSource is used as an intermediary. If you simply bind an ItemsControl to a collection, no “default view” will be created internally for you. Also, since the ItemCollection class doesn’t implement ICollectionView, it’s not possible to sort or filter non-data bound items that have been added to an ItemsControl.

Just like in WPF, Silverlight’s CollectionViewSource creates a view to wrap a collection when its Source property points to a new collection. In both platforms, it is not necessary to specify “Path=View” when binding to the CollectionViewSource – the binding does that automatically. Here’s the syntax you use to bind to a CollectionViewSource:

    <UserControl.Resources>
        <CollectionViewSource x:Key="cvs" />
    </UserControl.Resources>
    
    <ListBox ItemsSource="{Binding Source={StaticResource cvs}}" />

Notice that I didn’t have to specify “{Binding Source={StaticResource cvs}, Path=View}” to bind to the view exposed by the CollectionViewSource. Both syntaxes are equivalent, but the second is unnecessary – the binding engine knows to drill into the View property when given a CollectionViewSource.

In WPF, when the CollectionViewSource points to several collections throughout its life, it creates a view for each of them and remembers those views. My last post explains how this feature enables a common scenario that would be a bit of work to implement without CollectionViewSource. I’m very glad to say that this feature has also been implemented in Silverlight, so the selection behavior of my previous post’s WPF sample works the same way in Silverlight.

In fact, porting that sample to Silverlight was straightforward. The only difference was that in WPF, Bindings within the resources inherit the Window’s DataContext. This enabled me to write the following code/XAML to bind the CollectionViewSource’s Source to the Window’s DataContext:

    this.DataContext = new Mountains();
    
    <CollectionViewSource Source="{Binding}" x:Key="cvs1"/>

This same feature is not present in Silverlight 3. Here’s one equivalent way of implementing that behavior in Silverlight:

    <local:Mountains x:Key="mountains" />
    <CollectionViewSource Source="{Binding Source={StaticResource mountains}}" x:Key="cvs1"/>

If you have Silverlight 3 installed, you can see a Silverlight version of my previous post’s WPF sample in its own page or embedded below:

Download the Silverlight project (built with Silverlight 3).




Posted by Bea under Silverlight | Comments (4)

September 20, 2009

How can I port the WPF labeled pie chart to Silverlight?

Two posts ago I showed a possible solution to add labels to a WPF pie chart. In my last post, I explained some implementation details of that solution. In this blog post, I will show the steps I took to port the labeled pie chart to Silverlight.

The Silverlight and WPF teams do their best to ensure that porting a Silverlight application to WPF is a smooth experience. This is expected, since Silverlight is (for the most part) a subset of WPF. Porting WPF applications to Silverlight, on the other hand, can be pretty tricky. The Silverlight and WPF teams are not specifically supporting this scenario, but the reality is that in the real world, many people need to do just that. I decided to port my WPF labeled pie chart to Silverlight to see how many issues I would come across and to document the workarounds.

MeasureOverride is sealed in Silverlight

The first issue I encountered was the fact that Canvas’ MeasureOverride is sealed in Silverlight, so I couldn’t override it. I was overriding it in the PieChartLabelArea, where I had to know whether any of the child labels are associated with small arcs to help with the Auto display mode.

My workaround was to derive from Panel instead, since Panel’s MeasureOverride is not sealed. Since Panel doesn’t do automatic arranging, I had to also implement ArrangeOverride:

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement child in this.Children)
        {
            child.Arrange(new Rect(new Point(0, 0), child.DesiredSize));
        }
    
        return finalSize;
    }

In this scenario it was OK to replace the Canvas with a Panel because the WPF implementation wasn’t using any of Canvas’ functionality other than the automatic arranging. The contents of the labels are positioned inside a different Canvas that’s part of the ControlTemplate for PieChartLabel. When PieChartLabels are added to the PieChartLabelArea, the canvases in their templates are all placed at the origin of the label area, and each label is positioned correctly within its own canvas.

OverrideMetadata doesn’t exist in Silverlight

In WPF I was using the following code in the LabeledPieChart’s static constructor to set its default style key:

    static LabeledPieChart()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LabeledPieChart), new FrameworkPropertyMetadata(typeof(LabeledPieChart)));
    }

Since OverrideMetadata doesn’t exist in Silverlight, I used the instance constructor instead to set the DefaultStyleKey property:

    public LabeledPieChart()
    {
        this.DefaultStyleKey = typeof(LabeledPieChart);
    }

AddOwner doesn’t exist in Silverlight

In WPF, I was registering some DPs with the convenient AddOwner method. For example, the following line of code indicates that the Title property of Chart can be applied to LabeledPieChart as well:

    public static readonly DependencyProperty TitleProperty = Chart.TitleProperty.AddOwner(typeof(LabeledPieChart), null);

The workaround is to register a new DP with the same name in LabeledPieChart. The syntax is not much longer:

    public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(object), typeof(LabeledPieChart), new PropertyMetadata(String.Empty));

Vector doesn’t exist in Silverlight

The WPF version of this code makes use of the Vector class in several places. We rely on Vector in PieChartHelper to calculate the midpoint of a wedge’s arc, and in PieChartLabel to determine the three points needed in Connected mode.

As a workaround, I decided to bring a version of WPF’s Vector class into the Silverlight project. I simply copied this code from Reflector and tweaked it a bit.

FrameworkPropertyMetadataOptions.AffectsArrange doesn’t exist in Silverlight

In WPF, I specified that the DisplayMode property should invalidate arrange when set, which in turn was causing the label to be repositioned.

    public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register("DisplayMode", typeof(DisplayMode), typeof(PieChartLabel), new FrameworkPropertyMetadata(DisplayMode.ArcMidpoint, FrameworkPropertyMetadataOptions.AffectsArrange));

FrameworkPropertyMetadataOptions.AffectsArrange doesn’t exist in Silverlight, but we can work around it by invalidating arrange in the change handler for the DP. Here’s the corresponding code in Silverlight:

    public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register("DisplayMode", typeof(DisplayMode), typeof(PieChartLabel), new PropertyMetadata(DisplayMode.ArcMidpoint, DisplayModePropertyChanged));
    
    private static void DisplayModePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        PieChartLabel label = obj as PieChartLabel;
        if (label != null)
        {
            label.InvalidateArrange();
        }
    }

FrameworkPropertyMetadataOptions.AffectsParentMeasure doesn’t exist in Silverlight

In WPF, I specified that changing the Geometry property of PieChartLabel should affect the parent’s measure when I registered the DP:

    public static readonly DependencyProperty GeometryProperty = PieDataPoint.GeometryProperty.AddOwner(typeof(PieChartLabel), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsParentMeasure | FrameworkPropertyMetadataOptions.AffectsArrange, GeometryPropertyChanged));

The equivalent behavior in Silverlight can be done by adding a change handler for this DP that invalidates measure on the element’s parent:

    public static readonly DependencyProperty GeometryProperty = DependencyProperty.Register("Geometry", typeof(Geometry), typeof(PieChartLabel), new PropertyMetadata(null, GeometryPropertyChanged));
    
    private static void GeometryPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        PieChartLabel label = obj as PieChartLabel;
        if (label != null)
        {
            …
            FrameworkElement fe = label.Parent as FrameworkElement;
            if (fe != null)
            {
                fe.InvalidateMeasure();
            }
        }
    }

Template.FindName doesn’t exist in Silverlight

WPF has a useful method that allows us to find an element with a specified name within a template:

    PieChartLabelArea labelArea = chart.Template.FindName("LabelArea_PART", chart) as PieChartLabelArea;

Silverlight doesn’t include the FindName method. As a workaround, I wrote a helper method that searches the visual tree until it finds an element with the specified name. The following line of code returns the label area in Silverlight:

    PieChartLabelArea labelArea = TreeHelper.FindDescendent(chart, "LabelArea_PART") as PieChartLabelArea;

FrameworkElement.Unloaded doesn’t exist in Silverlight

In the WPF version of this project, I ensure that the labels are removed when the PieDataPoints are unloaded with the following code:

    pieDataPoint.Unloaded += delegate
    {
        labelArea.Children.Remove(label);
    };

However, in Silverlight, FrameworkElement does not have an Unloaded event. As a workaround, I check whether the PieDataPoint is still in the tree every time layout is updated. When I discover that the data point is no longer in the tree, I remove the corresponding label from the tree.

    pieDataPoint.LayoutUpdated += delegate
    {
        if (!pieDataPoint.IsInTree())
        {
            labelArea.Children.Remove(label);
        }
    };

And here’s how I implemented the IsInTree helper method:

    public static bool IsInTree(this FrameworkElement element)
    {
        var rootElement = Application.Current.RootVisual as FrameworkElement;
        while (element != null)
        {
            if (element == rootElement)
            {
                return true;
            }
            element = VisualTreeHelper.GetParent(element) as FrameworkElement;
        }
        return false;
    }

SnapsToDevicePixels doesn’t exist in Silverlight

I set the inherited property SnapsToDevicePixels to true on the label area in WPF to make sure all the labels have crisp borders – I don’t want antialiasing to blur any edges that fall between pixel boundaries. There’s no SnapsToDevicePixels property in Silverlight, but the workaround is easy: just leave it out! Silverlight introduces a new inherited property, UseLayoutRounding, which is true by default. The difference between the two properties is subtle (UseLayoutRounding affects layout sizes, while SnapsToDevicePixels doesn’t), but the effect is the same: both keep single-pixel borders sharp. Note that UseLayoutRounding was recently added to version 4 of WPF, but it’s false by default to maintain backward compatibility.

{x:Type …} syntax is not supported in Silverlight

The following syntax is supported only in WPF:

    <Style TargetType="{x:Type local:PieChartLabel}">

The syntax below is equivalent and is supported both in WPF and Silverlight:

    <Style TargetType="local:PieChartLabel">

Binding to AncestorType is not supported in Silverlight

Below you can see the control and data templates for the PieChartLabel in WPF:

    <Style TargetType="{x:Type local:PieChartLabel}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:PieChartLabel}">
                    <Canvas Name="Canvas_PART">
                        <ContentPresenter Name="Content_PART"/>
                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <StackPanel Background="LightGray">
                        <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:PieChartLabel}}, Path=FormattedRatio}" VerticalAlignment="Center" Margin="5,0,5,0" />
                    </StackPanel>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Within the DataTemplate, I use an AncestorType binding to display the FormattedRatio property defined on PieChartLabel. I couldn’t use a TemplateBinding because that would refer to properties of the ContentPresenter.

Silverlight doesn’t support AncestorType bindings. One possible solution to work around this issue could have been to use a binding to Self with a Converter, and within the converter walk up the visual tree until the PieChartLabel is found. However, the binding is evaluated before the PieChartLabel is added to the tree, so this approach doesn’t really help.

As a workaround, I merged the data template into the control template so that I could use a TemplateBinding to access the FormattedRatio.

    <Style TargetType="local:PieChartLabel">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PieChartLabel">
                    <Canvas Name="Canvas_PART">
                        <Polyline Name="Polyline_PART" StrokeThickness="{TemplateBinding LineStrokeThickness}" Stroke="{TemplateBinding LineStroke}" StrokeLineJoin="Round" />
                        <StackPanel Background="LightGray" Name="Content_PART">
                            <TextBlock Text="{TemplateBinding FormattedRatio}" VerticalAlignment="Center" Margin="5,0,5,0" />
                        </StackPanel>
                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

WPF’s Polyline rendering bug is not present in Silverlight

To end on a high note, I was able to remove the workaround for the Polyline rendering bug I struggled with in the WPF version of this code. As I explained in my previous post, if I placed a Polyline in the label’s template and modified its points whenever the label position changed, WPF would occasionally render the Polyline incorrectly. To work around the issue, I had to create a new Polyline every time the label was repositioned, which is not as efficient.

I was glad to see that this bug is not present in Silverlight. So I added the Polyline to the PieChartLabel’s template and simply changed its points in code. You can see the XAML containing the Polyline in the Silverlight Style in the previous section. The code that adds the points to the Polyline instead of creating a new Polyline can be found in the PositionConnected() method of PieChartLabel. This is a straightforward change, so I won’t show it here.


If you have Silverlight 3 installed, you can see a running version of this example in its own page or embedded below:

Download the Silverlight project (built with Silverlight 3).


Posted by Bea under Silverlight | Comments (12)

July 26, 2009

Data virtualization

My last post covered the current UI virtualization support on Silverlight and WPF. In this post, I will discuss another technique that helps improve performance when dealing with large data sets: data virtualization.

A control that uses data virtualization knows to keep in memory no more data items than the ones needed for display. For example, if a data-virtualized ListBox is data bound to 1,000,000 data items and only displays 100 at a single point in time, it only keeps about 100 data items in memory. If the user scrolls, the data-virtualized ListBox knows to swap the items in memory in a way that optimizes performance and memory consumption.

I talked about a possible solution that provides partial data virtualization for TreeView in a previous post. I discussed that solution in the context of WPF, but it could just as easily be used for Silverlight. The discussion in this post addresses data virtualization for non-hierarchical scenarios.

Data virtualization in WPF

WPF has no built-in generic support for data virtualization.

Fortunately, Paul McClean and Vincent Van Den Berghe have recently come up with approaches to work around this limitation (the link to Vincent’s code in the pdf is incorrect, so I’ve uploaded his code here). These two solutions haven’t been advertised enough, in my opinion! They’re both great solutions and easy to adapt to your specific scenario.

Paul and Vincent’s solutions are very similar to each other – they both implement a layer that is capable of managing data items that are kept in memory. This layer knows when to fetch more data items and when to let old data items be garbage collected, based on the user’s scrolling patterns.

Both solutions take advantage of the fact that when a WPF ListBox is data bound to an IList, it only accesses the list’s Count property and the data items it needs to display on the screen. Both Paul and Vincent implemented a “fake” collection that knows the overall count of the real collection and knows how to retrieve a particular data item, even if that data item is not in memory. This collection essentially manages a cache of items and, when asked for new data items, it’s capable of retrieving a new set of data items from the database, discarding data items that are no longer in use.

The implementation details are quite different, though (this is expected – neither of them was aware of the other one’s work).

Paul implemented his own collection called VirtualizingCollection<T>. This collection has a handle to an IItemsProvider, which is a class that knows how to fetch the overall item count and a consecutive list of items from the database (or web service, XML file or whatever data store you’re using). VirtualizingCollection<T> keeps the list of non-virtualized data items and decides when new items should be retrieved. Its indexer immediately returns a data item if it’s already in memory, or loads more data from the database if it’s not. Paul also implemented an extension to this collection called AsyncVirtualizingCollection<T> that provides the ability to do expensive query operations without blocking the UI thread.

Vincent implemented not only his own collection, but also a custom view type for that collection. His collection (VirtualList<T>) implements ICollectionViewFactory, which allows the collection to say that it wants WPF to create a VirtualListCollectionView<T> when binding to it. His view has a handle to the collection and knows to forward any data loading operations to the collection, as well as any sorting and filtering the user may have specified. The data item caching code is in VirtualListBase<T>, which is the base class for both the view and the collection. Vincent also implements a DataRefBase<T> class that wraps each data item, exposing all of its properties using the ICustomTypeDescriptor interface, and adding property change notifications if not present in the original data type.

At the end of this post, I link to an xbap that shows both solutions running side-by-side. Below I compare different aspects of these solutions, and discuss pros and cons of each solution when applicable.

Using these solutions in your project

If you want to use Paul’s solution, you need to provide your own implementation of IItemsProvider. This is where you would add code that retrieves the overall count and a page of items from your database. Then you just need to bind your control to a collection of type AsyncVirtualizingCollection<T>. Here is the example included in Paul’s sample code:

    public class DemoCustomerProvider : IItemsProvider<PaulsCustomer>
    {
        private readonly int _count;
        private readonly int _fetchDelay;
    
        public DemoCustomerProvider(int count, int fetchDelay)
        {
            _count = count;
            _fetchDelay = fetchDelay;
        }
    
        public int FetchCount()
        {
            Thread.Sleep(_fetchDelay);
            return _count;
        }
    
        public IList<PaulsCustomer> FetchRange(int startIndex, int count)
        {
            Thread.Sleep(_fetchDelay);
        
            List<PaulsCustomer> list = new List<PaulsCustomer>();
            for (int i = startIndex; i < startIndex + count; i++)
            {
                PaulsCustomer customer = new PaulsCustomer { Id = i, Name = "Customer " + i };
                list.Add(customer);
            }
            return list;
        }
    }
    
    DemoCustomerProvider customerProvider = new DemoCustomerProvider(numItems, fetchDelay);
    sample1.DataContext = new AsyncVirtualizingCollection<PaulsCustomer>(customerProvider, pageSize, pageTimeout);

In Vincent’s solution, you need to define a “Load” function somewhere in your ViewModel and pass that as a parameter to the collection. For example:

    private int Load(SortDescriptionCollection sortDescriptions, Predicate<object> filter, VincentsCustomer[] customers, int startIndex)
    {
        Thread.Sleep(fetchDelay);
    
        // Sorting
        bool isDescending = sortDescriptions != null && sortDescriptions.Count > 0 && sortDescriptions[0].Direction == ListSortDirection.Descending;
        int customerIndex;
    
        for (int i = 0; startIndex < numItems && i < customers.Length; ++i, ++startIndex)
        {
            customerIndex = isDescending ? numItems – startIndex – 1 : startIndex;
            customers[i] = new VincentsCustomer { Id = customerIndex, Name = "Customer " + customerIndex };
        }
    
        return numItems;
    }
    
    sample2.DataContext = new VirtualList<VincentsCustomer>(Load, 6 /* # of pages in memory at the same time */, pageSize);

Caching

Paul’s solution divides the original collection in pre-defined pages. When an item is accessed, Paul’s solution brings into memory the page that includes it, as well as the previous or next page, depending on whether the item belongs to the first or second half of its page. Paul also keeps track of the time each page is accessed. Every time a new item is retrieved, pages that haven’t been touched for a certain amount of time are discarded.

Vincent keeps a linked list of pages in memory. When a new page is brought into memory or an existing page is accessed, it is moved to the beginning of the list. If the number of pages exceeds a specified limit, the pages at the end of the list are discarded.

Sorting and filtering

Paul’s solution doesn’t address sorting and filtering. One way to add this functionality to his code is to change the FetchRange method of IItemsProvider to accept sorting and filtering information. In your implementation of IItemsProvider, you would implement FetchRange taking that information into account.

Because Vincent implements his own view, you can add SortDescriptions and a filter delegate as usual. Vincent takes care of passing this information to the Load method. It’s up to you to create a database query that incorporates that information in your implementation of Load.

Threading

Paul’s solution fetches the data in a second thread, therefore it doesn’t block the UI. The code that enables this scenario is in AsyncVirtualizingCollection<T>.

In Vincent’s solution, everything happens synchronously. This means that the UI will be unresponsive while the database is being queried.

DataTemplates

In Paul’s solution, we can use implicit DataTemplates as usual (a DataTemplate is “implicit” when it specifies a DataType but no key).

In Vincent’s solution, the items in the collection are of type DataRefBase<T>, which wraps your original type and provides property change notifications. Unfortunately, you can’t use an implicit template in this scenario because the type is generic. So, in this post’s project, I opted to use the DataTemplate explicitly when using Vincent’s solution (I refer to the DataTemplate using an explicit key).

(XAML 2009 adds support for generic types, but this feature is not yet available for compiled XAML scenarios. You can read more about this in Rob Relyea’s blog.)

Master-detail Scenario

Vincent’s solution works well when I implement a master-detail scenario by synchronizing the current and selected items:

    <ListView ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True" …>
    <ContentControl Content="{Binding Path=/}" … />

Paul’s solution doesn’t work with the XAML above because WPF’s ListCollectionView calls IndexOf to track the current item, and Paul’s collection doesn’t support IndexOf. As an alternative, I implemented the master-detail scenario by binding the ContentControl’s Content to the ListView’s SelectedItem:

    <ListView ItemsSource="{Binding}" Name="lv1" …>
    <ContentControl Content="{Binding ElementName=lv1, Path=SelectedItem}" … />

This second master-detail implementation works just as well as the first, and in fact, many people find the second solution easier to understand.

Selection

One shortcoming of Paul’s solution is the fact that a “collection reset” notification is raised every time a new page is loaded. As a result, selection is lost every time a new page is loaded. For example, if a user holds the down-arrow key to scroll quickly through a list, selection will jump back to the top of the list when a new page of data items is loaded.

This problem might be addressed by providing more granular collection change notifications when new data is available (look at the FireCollectionReset method in AsyncVirtualizingCollection<T>).

Summary

When comparing Paul’s solution to Vincent’s, you may prefer one over the other, depending which features you find valuable. An ideal solution would combine the best parts of both. Let me know if you come up with a good hybrid!

Data virtualization in Silverlight

Like WPF, Silverlight does not have built-in support for data virtualization based on scrolling. Unlike WPF, Silverlight has support for data virtualization through paging using PagedCollectionView. I won’t talk about PagedCollectionView here – instead I’ll focus the discussion on the scrolling based solution.

Vincent’s solution requires several features that are not present in the subset of .NET supported by Silverlight, but Paul’s solution compiles in Silverlight with very minor changes. However, the implementation of views in Silverlight is less optimized than in WPF, preventing this approach to data virtualization from working.

The internal implementation of views in WPF accesses only the items that it needs for display (as well as the count of the collection). This is what makes custom data virtualization solutions conceptually easy – we only need to keep in memory the data items that are accessed, and can discard all others.

The implementation of views in Silverlight, on the other hand, accesses all data items in the original collection at load time, independent of how many items it displays on the screen. This affects performance at load time in all scenarios, but because the delay is proportional to the count of the collection, it especially affects scenarios with large data sets. There is no way around this, other than re-implementing large portions of Silverlight’s collection views code. If you use .NET Reflector, you can look at this code in the InitializeSnapshot method of EnumerableCollectionView. This is, in my opinion, a big limitation of Silverlight that I am hoping will be addressed soon.

This fact increases the load time significantly, but by itself it shouldn’t cause the UI to hang for too long (depending on the number of items in the collection) because Paul’s solution loads pages asynchronously. However, because the collection is reset every time a new page is brought into memory, the effects of looping through all items are magnified. For example, if your collection has 100,000 items and the page size is 100 items, InitializeSnapshot’s loop will bring 1,000 pages into memory at load time, asynchronously. As each of those pages is ready to be loaded, a collection reset is triggered, causing Silverlight to loop through all 100,000 items again, causing all pages to be loaded into memory again. At the end of this post, I provide a link to a Silverlight project with Paul’s data virtualization implementation so you can see it hang and debug it in case you’re curious.

I am not aware of any custom data virtualization solution based on scrolling that works with Silverlight at this point, so here’s a fun challenge for you! Please send me email if you come up with a solution for this problem.


You can run an xbap showing both data virtualization solutions side by side, and you can download the corresponding WPF project.

You can also download a Silverlight project that shows data virtualization *not* working.

Update: If you’re looking for a data virtualization solution for WPF, make sure you also read my more recent post.


Posted by Bea under Silverlight, WPF | Comments (24)

July 11, 2009

UI Virtualization

Today’s post is motivated by a scenario that’s common in business applications: displaying and interacting with a large data set. We’ll quickly run into performance problems if we use the naïve approach of loading the entire data set into memory and creating UI elements for each data item. Fortunately, there are some things we can do to make sure our applications perform well, even with extremely large data sets.

The first approach is called “UI virtualization.” A control that supports UI virtualization is smart enough to create only the UI elements needed to display the data items that are actually visible on screen. For example, suppose we have a scrolling ListBox that is data bound to 1,000,000 items but only 100 are visible at any time. Without UI virtualization, the ListBox would create a million ListBoxItems – a slow process – and include them in the UI, even though only a hundred of them are visible. With UI virtualization, on the other hand, the ListBox would only create 100 ListBoxItems (or a few more, to improve scrolling performance).

The second approach, called “data virtualization,” goes one step further. A control that uses data virtualization doesn’t load all the data items into memory. Instead, it only loads the ones it needs to display. In our ListBox example above, a solution using data virtualization would only keep about 100 data items in memory at any given time.

In this post, I will talk about the current level of support for UI virtualization in Silverlight and WPF. In my next post, I will discuss data virtualization.

UI virtualization in Silverlight

Silverlight 3 just shipped! I am very pleased to say that with this release, Silverlight’s ListBox now supports UI virtualization! This feature was not part of the beta release – it’s brand new in the final release. I have to admit it’s my favorite new feature of Silverlight 3.

It was possible to work around the lack of UI virtualization before (in fact, I wrote a virtualized ListBox in Silverlight a year ago), but it wasn’t straightforward. I’m very glad that this feature is now part of Silverlight 3!

If you’re familiar with the UI virtualization provided by WPF’s controls, you’re probably curious about the level of support for virtualization in Silverlight. Just like WPF, Silverlight supports container recycling, but there is no support for deferred scrolling or for UI virtualization with hierarchical data. I will expand on these concepts below while discussing UI virtualization in WPF.

UI virtualization in WPF

WPF has supported UI virtualization for a long time. The ListBox and ListView controls use VirtualizingStackPanel as their default panel, and VSP knows to create UI containers (ListBoxItems or ListViewItems) when new items are about to be shown in the UI, and to discard those containers when items are scrolled out of view.

If you’re using another ItemsControl (such as ComboBox) that doesn’t use VirtualizingStackPanel by default, you can change the panel used by the control in a very simple way:

    <ComboBox ItemsSource="{Binding}">
        <ComboBox.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel />
            </ItemsPanelTemplate>
        </ComboBox.ItemsPanel>
    </ComboBox>

The UI virtualization support in .NET 3.5 was already pretty solid, but the WPF team decided to further improve UI virtualization in .NET 3.5 SP1. With that release, the following new features were introduced:

Container recycling

.NET 3.5 SP1 supports the reuse of UI containers already in memory. For example, imagine that when a ListBox is loaded, 30 ListBoxItems are created to display the visible data. When the user scrolls the ListBox, instead of discarding ListBoxItems that scroll out of view and creating new ones for the data items that scroll into view, WPF reuses the existing ListBoxItems. This results in significant performance improvements compared to previous versions because it decreases the time spent initializing ListBoxItems. And since garbage collection is not instantaneous, it also reduces the number of ListBoxItems in memory at one time.

You can enable container recycling by setting the attached property “VirtualizingStackPanel.VirtualizationMode” to “Recycling” on your control:

    <ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" … />

To maintain backwards compatibility with the behavior of earlier versions, container recycling is disabled by default (the default VirtualizationMode is “Standard”). As a rule of thumb, I suggest setting this property every time you create a control that requires scrolling to display data items.

Silverlight 3 also supports container recycling, but it’s enabled by default for ListBox, so there is no need to set the “VirtualizationMode” to “Recycling” explicitly. This is a slight incompatibility between the two frameworks. Because I frequently switch back and forth between Silverlight and WPF, I’d rather be explicit about it every time so that I don’t forget it when I need it.

Deferred scrolling

“Deferred scrolling” is a feature that allows the user to drag the scroll bar thumb around without changing the displayed items until the scroll bar thumb is released. This improves the application’s perceived responsiveness to scrolling when the items are displayed using complex templates, though of course, the user can’t see the items they’re scrolling through.

With .NET 3.5 SP1, it is possible to enable deferred scrolling by setting an attached property on the control:

    <ListBox ScrollViewer.IsDeferredScrollingEnabled="True" … />

Again, for backwards compatibility reasons, this feature is disabled by default. Deferred scrolling is not supported in Silverlight 3.

Hierarchical data

In .NET 3.5 SP1, the WPF team extended UI virtualization to TreeView by adding support for hierarchical data to VirtualizingStackPanel. As a consequence, the container recycling and deferred scrolling features discussed above also apply to hierarchical data. UI virtualization is disabled by default in TreeView – here’s how you enable it:

    <TreeView VirtualizingStackPanel.IsVirtualizing="True" … />

This property is useful not just for TreeView, but for any control that uses VirtualizingStackPanel and that doesn’t set IsVirtualizing to true (ItemsControl, for example). ListBox already sets IsVirtualizing to True by default, so there is no need to set it explicitly.

Silverlight 3 doesn’t support UI virtualization for hierarchical data. It also doesn’t allow you to set the “IsVirtualizing” property. If your Silverlight control scrolls and uses a VirtualizingStackPanel to display non-hierarchical data, UI virtualization is enabled automatically.

Limitations

.NET 3.5 SP1 fixed many previous limitations on UI virtualization, but a couple still remain:

  • ScrollViewer currently allows two scrolling modes: smooth pixel-by-pixel scrolling (CanContentScroll = false) or discrete item-by-item scrolling (CanContentScroll = true). Currently WPF supports UI virtualization only when scrolling by item. Pixel-based scrolling is also called “physical scrolling” and item-based scrolling is also called “logical scrolling”.
  • When using data binding’s “Grouping” feature, there is no support for UI virtualization.

These are really the same limitation. If you look at the default style for ListBox, ListView and ComboBox, you will find the following trigger:

    <Trigger Property="IsGrouping" Value="true">
        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
    </Trigger>

The implementation of Grouping assumes that each group is a separate item in the ItemsControl that contains it. Since each group can (and typically does) have many sub-items, scrolling by item would result in a really bad user experience – scrolling down a bit would cause a big jump to the top of the next group. That’s why the team decided to switch to pixel based scrolling when displaying grouped data. The unfortunate consequence is that no UI virtualization is supported when grouping.

I’m often asked if there is a way to work around this limitation. Well, anything is possible, but there is no *easy* workaround. You would have to re-implement significant portions of the current virtualization logic to combine pixel-based scrolling with UI virtualization. You would also have to solve some interesting problems that come with it. For example, how do you calculate the size of the thumb when the item containers have different heights? (Remember that you don’t know the height of the virtualized containers – you only know the height of the containers that are currently displayed.) You could assume an average based on the heights you do know, or you could keep a list with the item heights as items are brought into memory (which would increase accuracy of the thumb size as the user interacts with the control). You could also decide that pixel-based scrolling only works with items that are of the same height – this would simplify the solution. So, yes, you could come up with a solution to work around this limitation, but it’s not trivial.

And this brings me to another change introduced in Silverlight 3. Silverlight 2’s ListBox used to support only pixel-based scrolling, but with the introduction of UI virtualization in Silverlight 3, the default scrolling mode for ListBox is now item-based. Unlike WPF, Silverlight’s ScrollViewer doesn’t have a “CanContentScroll” property. In Silverlight 3, if your ListBox uses VSP it will scroll by item and have virtualization enabled, and if you change it to use StackPanel instead, it will scroll by pixel and have virtualization disabled.

Posted by Bea under Silverlight, WPF | Comments (29)

December 30, 2008

How can I expand items in a TreeView? – Part III

This is the third of a three-part series about expanding TreeViewItems. In the first post I explained how to use an implicit Style to expand all TreeViewItems at load time. In the second post I showed how you can drive expansion and selection of items using an intermediate data source. In this post, I will explain how you can expand and select TreeViewItems using the dispatcher.

Expanding all TreeViewItems by setting the IsExpanded property on the items directly is not as simple as doing a tree walk and marking this property as you go. The problem is that after expanding a TreeViewItem, you need to return control to WPF or Silverlight so that the children TreeViewItems can be instantiated, before it’s their turn to be expanded. Fortunately, the Dispatcher can be used on both of these technologies to ensure the instantiation of the TreeViewItems.

WPF

Those of you who have experience with previous Microsoft technologies may have used the “DoEvents” method in the past, which performs a non-blocking wait. WPF doesn’t ship an equivalent method, but it’s easy to implement similar behavior using the Dispatcher. I like using a version of this method that takes a DispatcherPriority, so that I have more control over when to resume execution of my code. You can take a look at the code I use below:

    internal static void WaitForPriority(DispatcherPriority priority)
    {
        DispatcherFrame frame = new DispatcherFrame();
        DispatcherOperation dispatcherOperation = Dispatcher.CurrentDispatcher.BeginInvoke(priority, new DispatcherOperationCallback(ExitFrameOperation), frame);
        Dispatcher.PushFrame(frame);
        if (dispatcherOperation.Status != DispatcherOperationStatus.Completed)
        {
            dispatcherOperation.Abort();
        }
    }
    
    private static object ExitFrameOperation(object obj)
    {
        ((DispatcherFrame)obj).Continue = false;
        return null;
    }

In the code above, you can see that I create a new DispatcherFrame and only exit from that frame once dispatcher operations of the specified priority are reached. This way, I will give WPF a chance to execute anything with a priority higher than the one passed as a parameter before continuing executing the next line of code. For example, in the following code I ensure that all jobs with priority higher than Background have been executed by the time I call MyMethod2.

    MyMethod1();
    WaitForPriority(DispatcherPriority.Background);
    MyMethod2();

If there is other work for the dispatcher to do at the priority specified, it is not guaranteed that that work will happen before starting execution of MyMethod2. Since the “ExitFrameOperation” method is BeginInvoked at Background priority in this case, it is possible that other tasks at the same priority will be executed after this one. For this reason, if you want to make sure all operations at Background priority have been executed, you should pass priority ContextIdle instead (the next priority level).

Now that you understand this very useful WaitForPriority method, we can look at how we can use it to expand all items in a TreeView. Expanding the first level of TreeViewItems is easy, but in order to expand the second level of items, we need to make sure that the TreeViewitems are fully instantiated. This can only be achieved by returning control back to WPF just long enough for those items to be instantiated, and then continue execution. This is the perfect job for WaitForPriority.

In the code below, I do a full non-recursive depth-first tree traversal, and for each item I encounter, I set IsExpanded to true and wait in a non-blocking way for the child TreeViewItems to be instantiated.

    private void ExpandAll(object sender, RoutedEventArgs e)
    {
        ApplyActionToAllTreeViewItems(itemsControl =>
        {
            itemsControl.IsExpanded = true;
            DispatcherHelper.WaitForPriority(DispatcherPriority.ContextIdle);
        },
        treeView);
    }
    
    private void ApplyActionToAllTreeViewItems(Action<TreeViewItem> itemAction, ItemsControl itemsControl)
    {
        Stack<ItemsControl> itemsControlStack = new Stack<ItemsControl>();
        itemsControlStack.Push(itemsControl);
    
        while (itemsControlStack.Count != 0)
        {
            ItemsControl currentItem = itemsControlStack.Pop() as ItemsControl;
            TreeViewItem currentTreeViewItem = currentItem as TreeViewItem;
            if (currentTreeViewItem != null)
            {
                itemAction(currentTreeViewItem);
            }
            if (currentItem != null) // this handles the scenario where some TreeViewItems are already collapsed
            {
                foreach (object dataItem in currentItem.Items)
                {
                    ItemsControl childElement = (ItemsControl)currentItem.ItemContainerGenerator.ContainerFromItem(dataItem);
                    itemsControlStack.Push(childElement);
                }
            }
        }
    }

My explanation about collapsing TreeViewItems in the previous post is applicable to this scenario too. There are basically two ways you can collapse the item: you can either collapse just the top level items (in which case expanding them again will keep the previous expansion state) or you can collapse every single item in the tree (in which case the previous expansion state is lost).

    private void CollapseTopLevel(object sender, RoutedEventArgs e)
    {
        foreach (Taxonomy item in treeView.Items)
        {
            TreeViewItem tvi = treeView.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
            tvi.IsExpanded = false;
        }
    }
    
    private void CollapseAll(object sender, RoutedEventArgs e)
    {
        ApplyActionToAllTreeViewItems(itemsControl => itemsControl.IsExpanded = false, treeView);
    }

Expanding and selecting one tree node is a bit more complex using the dispatcher than it was using the intermediate data source. In the previous post, I was able to use recursion to look for the item, and as I returned from each level of the recursion, I expanded the item. So I started by expanding the bottom level node, and worked my way to the top. When using the intermediate data source solution this order didn’t matter because all items were updated in the same layout pass.

However, when interacting with the TreeViewItems directly, I always need to start expanding the nodes from the top and wait for the next level of nodes to be instantiated before proceeding. So the simple algorithm from the previous post won’t help me here. The solution is to do this in two parts: first I navigate the whole tree using recursion, find the item to expand, and as I return from each level of recursion, I create a collection with the parent hierarchy. Once I have that information, I can now start from the top of the tree and expand each TreeViewItem that corresponds to a data item in the collection.

    private void SelectOne(object sender, RoutedEventArgs e)
    {
        ArrayList treeOfLifeCollection = (ArrayList)this.Resources["treeOfLife"];
        Taxonomy elementToExpand = ((Taxonomy)treeOfLifeCollection[2]).Subclasses[3].Subclasses[0].Subclasses[0].Subclasses[0];
    
        foreach (Taxonomy firstLevelDataItem in treeView.Items)
        {
            Collection<Taxonomy> superclasses = GetSuperclasses(firstLevelDataItem, elementToExpand);
            if (superclasses != null)
            {
                // Expand superclasses
                TreeViewItem parentTreeViewItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(firstLevelDataItem);
                parentTreeViewItem.IsExpanded = true;
                DispatcherHelper.WaitForPriority(DispatcherPriority.Background);
    
                foreach (Taxonomy superclassToExpand in superclasses.Skip(1))
                {
                    TreeViewItem treeViewItemToExpand = (TreeViewItem)parentTreeViewItem.ItemContainerGenerator.ContainerFromItem(superclassToExpand);
                    treeViewItemToExpand.IsExpanded = true;
                    DispatcherHelper.WaitForPriority(DispatcherPriority.Background);
                    parentTreeViewItem = treeViewItemToExpand;
                }
    
                // Select node
                TreeViewItem treeViewItemToSelect = (TreeViewItem)parentTreeViewItem.ItemContainerGenerator.ContainerFromItem(elementToExpand);
                treeViewItemToSelect.IsSelected = true;
            }
        }
    }
    
    private Collection<Taxonomy> GetSuperclasses(Taxonomy currentItem, Taxonomy itemToLookFor)
    {
        if (itemToLookFor == currentItem)
        {
            Collection<Taxonomy> results = new Collection<Taxonomy>();
            return results;
        }
        else
        {
            foreach (Taxonomy subclass in currentItem.Subclasses)
            {
                Collection<Taxonomy> results = GetSuperclasses(subclass, itemToLookFor);
                if (results != null)
                {
                    results.Insert(0, currentItem);
                    return results;
                }
            }
            return null;
        }
    }

If you were able to change your data source, a simpler alternative to this algorithm would be to add parent pointers to each data item. If you had parent pointers, instead of using recursion in the GetSuperclasses method, you could find the parent hierarchy with a simple for loop. If you were not able to change the data source but your data source used an ObservableCollection<T> to store the children of each data item, yet another option would be to add an intermediate data source that listens to collection changes in the original data, and adds parent pointers to the intermediate data when items are added. However, I didn’t want to provide a solution that assumes you can change your data source, because often you can’t. And I didn’t want to assume your source uses ObservableCollection<T> because often it doesn’t. I am certain that if you have the luxury of parent pointers or collection change notifications, you will be able to write the corresponding code easily.

I’d provide a link to the running xbap here, but that’s not possible for this example because DispatcherFrame can’t be used in the partial-trust security model of an xbap. Instead, you’ll have to build the WPF example from the source code provided at the end of this post.

Silverlight

The Silverlight version of the Dispatcher solution is quite a bit different from the WPF one. Silverlight does not have DispatcherFrame or DispatcherPriority, so there is no way to write a helper method similar to DoEvents. Fortunately, the Silverlight Dispatcher has a BeginInvoke method that I can use to return control to Silverlight, and allow it to instantiate the next level of TreeViewItems before continuing. Take a look at the code below. By calling MyMethod2 asynchronously, I am ensuring that control is returned to Silverlight before MyMethod2 is executed. This is the technique I will use to allow Silverlight to instantiate the next level of TreeViewItems before I can expand them.

    MyMethod1();
    myElement.Dispatcher.BeginInvoke(MyMethod2);

You can see this technique being using to expand all TreeViewItems:

    private void ExpandAll(object sender, RoutedEventArgs e)
    {
        for (int i = 0; i < treeView.Items.Count; i++)
        {
            ExpandAllTreeViewItems((TreeViewItem)treeView.ItemContainerGenerator.ContainerFromIndex(i));
        }
    }
    
    private void ExpandAllTreeViewItems(TreeViewItem currentTreeViewItem)
    {
        if (!currentTreeViewItem.IsExpanded)
        {
            currentTreeViewItem.IsExpanded = true;
            currentTreeViewItem.Dispatcher.BeginInvoke(() => ExpandAllTreeViewItems(currentTreeViewItem));
        }
        else
        {
            for (int i = 0; i < currentTreeViewItem.Items.Count; i++)
            {
                TreeViewItem child = (TreeViewItem)currentTreeViewItem.ItemContainerGenerator.ContainerFromIndex(i);
                ExpandAllTreeViewItems(child);
            }
        }
    }

Similarly to the previous solutions, I show both how you can collapse all items or just the top level. Since we don’t have to wait for TreeViewItems to be instantiated when collapsing all items, it is not necessary to use BeginInvoke. Any tree walking algorithm would work.

    private void CollapseAll(object sender, RoutedEventArgs e)
    {
        for (int i = 0; i < treeView.Items.Count; i++)
        {
            CollapseAllTreeViewItems((TreeViewItem)treeView.ItemContainerGenerator.ContainerFromIndex(i));
        }
    }
    
    private void CollapseAllTreeViewItems(TreeViewItem rootTreeViewItem)
    {
        Stack<TreeViewItem> treeViewItemsStack = new Stack<TreeViewItem>();
        treeViewItemsStack.Push(rootTreeViewItem);
        while (treeViewItemsStack.Count != 0)
        {
            TreeViewItem current = treeViewItemsStack.Pop();
            current.IsExpanded = false;
    
            for (int i = 0; i < current.Items.Count; i++)
            {
                treeViewItemsStack.Push(current.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem);
            }
        }
    }
    
    private void CollapseTopLevel(object sender, RoutedEventArgs e)
    {
        // This iterates through the three top-level items only.
        foreach (Taxonomy item in treeView.Items)
        {
            TreeViewItem tvi = treeView.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
            tvi.IsExpanded = false;
        }
    }

The code for expanding and selecting one item is also quite a bit different, and a bit more complex. In WPF, once we had all superclasses, we could navigate down the TreeView hierarchy with a for loop, as long as we remembered to return control to WPF after expanding each TreeViewItem. In Silverlight, I had to introduce a new ExpandPathAndSelectLast method that calls itself using BeginInvoke, giving an opportunity for Silverlight to create the next level of TreeViewItems between method calls. I don’t show the GetSuperclasses method again here, since it’s the same as the WPF version.

    private void SelectOne(object sender, RoutedEventArgs e)
    {
        ObjectCollection treeOfLifeCollection = (ObjectCollection)this.Resources["treeOfLife"];
        Taxonomy elementToExpand = ((Taxonomy)treeOfLifeCollection[2]).Subclasses[3].Subclasses[0].Subclasses[0].Subclasses[0];
    
        foreach (Taxonomy firstLevelDataItem in treeView.Items)
        {
            Collection<Taxonomy> superclasses = GetSuperclasses(firstLevelDataItem, elementToExpand);
            if (superclasses != null)
            {
                TreeViewItem parentTreeViewItem = (TreeViewItem)treeView.ItemContainerGenerator.ContainerFromItem(firstLevelDataItem);
                ExpandPathAndSelectLast(parentTreeViewItem, superclasses.Skip(1).GetEnumerator(), elementToExpand);
            }
        }
    }
    
    private void ExpandPathAndSelectLast(TreeViewItem currentTreeViewItem, IEnumerator enumerator, object itemToSelect)
    {
        if (!currentTreeViewItem.IsExpanded)
        {
            currentTreeViewItem.IsExpanded = true;
            currentTreeViewItem.Dispatcher.BeginInvoke(() => ExpandPathAndSelectLast(currentTreeViewItem, enumerator, itemToSelect));
        }
        else if (enumerator.MoveNext())
        {
            object dataItem = enumerator.Current;
            TreeViewItem nextContainer = (TreeViewItem)currentTreeViewItem.ItemContainerGenerator.ContainerFromItem(dataItem);
            ExpandPathAndSelectLast(nextContainer, enumerator, itemToSelect);
        }
        else
        {
            TreeViewItem treeViewItemToSelect = (TreeViewItem)currentTreeViewItem.ItemContainerGenerator.ContainerFromItem(itemToSelect);
            treeViewItemToSelect.IsSelected = true;
        }
    }

This is all the code you need to expand and collapse TreeViewItems.

If you have Silverlight 2 installed, you can see a running version of this example in its own page or embedded below:


Which TreeView expansion solution should I use?

Now that you know of three ways to expand, collapse and select items in a TreeView, you’re faced with the decision of which one you should use in your project. Below I show a quick bullet-point list of the pros and cons of each solution. Hopefully this will help you make the right decision.

Solution 1 – Expand all TreeViewItems using an implicit style

Pros:

  • Really really simple to implement.

Cons:

  • It is only useful to expand all TreeViewItems at app startup.

Solution 2 – Expand, collapse and select TreeViewItems using an intermediate data source

Pros:

  • This is the fastest way to expand items, since they all get expanded in one layout pass. If you are binding your TreeView to a large amount of data, this may be your best option.

Cons:

  • If your data source is complex, it may be very hard to write an intermediate data source.

Solution 3 – Expand, collapse and select TreeViewItems using the Dispatcher

Pros:

  • It’s independent of the data source, so you can reuse the code in TreeViews bound to different data sources (in fact, you can even add these as extension methods on TreeView).
  • If you don’t have a lot of data, the visual effect of expanding one hierarchy level per layout pass can be fun.

Cons:

  • If your data has a deep hierarchy, it will be very slow to expand the items. It will take as many layout passes as levels in the hierarchy.

Download the WPF project (built with .NET 3.5 SP1).

Download the Silverlight project (built with Silverlight 2).


Posted by Bea under Silverlight, WPF | Comments (3)

November 13, 2008

How can I expand items in a TreeView? – Part II

For those of you in Europe, I will be giving a talk at the Oredev conference in Malmo, Sweden, on Thursday the 20th of November. I hope I’ll get to meet some of you there.

    

    

In my last post, I showed how you can expand all items in a TreeView at load time. In WPF, this can be done by simply adding an implicit style to the resources, and in Silverlight we need a little help from ImplicitStyleManager to achieve the same behavior.

However, applications typically allow more complex interaction with a TreeView. In particular, they often permit users to expand all nodes, collapse all nodes, and expand the tree to reveal a particular node. I will show you one way to accomplish these tasks in this post, and a different way in my next post.

For the first approach, I will show you how to add an intermediate data layer to your application that adds UI-specific functionality on top of the data source. The idea of having an intermediate data layer isn’t new, and is explained in great detail by John Gossman in the context of WPF and Nikhil Kothari in the context of Silverlight.

WPF

I used the same “Taxonomy” data source in this post that you may already be familiar with from my previous post or from the Silverlight Toolkit’s sample pages.

For this sample, I decided that I wanted to allow users to perform three operations on the TreeView: expand all items, collapse all items, and expand just enough items to select a particular data item. To make these operations possible, I need a data source that has IsExpanded and IsSelected properties in each data item, so that I can data bind the corresponding properties of each TreeViewItem. Since my original data source does not contain these properties, I introduced an intermediate data source (sometimes called a view model) that has the properties I need.

There are a couple of ways to create an intermediate data source: you can either wrap the original data (containment) or you can derive from it (inheritance). Deriving may not be an option, if the data source is sealed or already has subclasses, so I chose the more general solution of containment for this sample. Here’s what my intermediate data source looks like at this point:

    public abstract class TaxonomyViewModel : INotifyPropertyChanged
    {
        public Taxonomy Taxonomy { get; private set; }
    
        private bool isExpanded;
        public bool IsExpanded
        {
            get { return isExpanded; }
            set
            {
                isExpanded = value;
                OnPropertyChanged("IsExpanded");
            }
        }
    
        private bool isSelected;
        public bool IsSelected
        {
            get { return isSelected; }
            set
            {
                isSelected = value;
                OnPropertyChanged("IsSelected");
            }
        }
        …
    }

Although wrapping is more flexible, it requires forwarding of properties and methods to the actual data items. I exposed the Classification and Rank properties of Taxonomy in the TaxonomyViewModel class, but the Subclasses collection presented a problem: a TaxonomyViewModel has as its “Subclasses” other TaxonomyViewModels, but a Taxonomy has “Subclasses” of type Taxonomy. For this reason, I had to create a parallel collection of the right type, and I had to make sure that changes to the collection in TaxonomyViewModel are propagated to Taxonomy.

    private TaxonomyViewModelCollection subclasses;
    public Collection<TaxonomyViewModel> Subclasses { get { return subclasses; } }
    
    private class TaxonomyViewModelCollection : Collection<TaxonomyViewModel>
    {
        private Collection<Taxonomy> originalCollection;
    
        public TaxonomyViewModelCollection(Collection<Taxonomy> originalCollection)
        {
            this.originalCollection = originalCollection;
        }
    
        protected override void InsertItem(int index, TaxonomyViewModel item)
        {
            base.InsertItem(index, item);
            originalCollection.Insert(index, item.Taxonomy);
        }
        …
    }

My next step was to add a method that expands all nodes. This method needs to traverse the whole hierarchy of data and set IsExpanded to true on all nodes. Any tree traversal algorithm would work – I chose to use a Stack to do a non-recursive depth-first traversal.

    public void ExpandAll()
    {
        ApplyActionToAllItems(item => item.IsExpanded = true);
    }
    
    private void ApplyActionToAllItems(Action<TaxonomyViewModel> itemAction)
    {
        Stack<TaxonomyViewModel> dataItemStack = new Stack<TaxonomyViewModel>();
        dataItemStack.Push(this);
    
        while (dataItemStack.Count != 0)
        {
            TaxonomyViewModel currentItem = dataItemStack.Pop();
            itemAction(currentItem);
            foreach (TaxonomyViewModel childItem in currentItem.Subclasses)
            {
                dataItemStack.Push(childItem);
            }
        }
    }

Now that the code for traversing the tree is already in place, collapsing all TreeViewItems can be done in one line. The result of setting IsExpanded to false in all nodes will have effect in the same layout pass (since I don’t return control to Silverlight during the tree traversal), so the order in which the IsExpanded properties are set does not matter. All items in the TreeView will collapse at the same time, in one layout pass.

    public void CollapseAll()
    {
        ApplyActionToAllItems(item => item.IsExpanded = false);
    }

And finally, I added a method that, given a data item, expands all the items in its ancestor chain. This method uses recursion to search for the data item passed as a parameter. Once the item is found, its ancestor chain gets expanded as the recursive call stack unwinds.

    public bool ExpandSuperclasses(TaxonomyViewModel itemToLookFor)
    {
        return ApplyActionToSuperclasses(itemToLookFor, superclass => superclass.IsExpanded = true);
    }

    private bool ApplyActionToSuperclasses(TaxonomyViewModel itemToLookFor, Action<TaxonomyViewModel> itemAction)
    {
        if (itemToLookFor == this)
        {
            return true;
        }
        else
        {
            foreach (TaxonomyViewModel subclass in this.Subclasses)
            {
                bool foundItem = subclass.ApplyActionToSuperclasses(itemToLookFor, itemAction);
                if (foundItem)
                {
                    itemAction(this);
                    return true;
                }
            }
            return false;
        }
    }

And that’s all for the intermediate data source. Now we need to hook this up to the UI. I started by adding three buttons that call the methods I just wrote. Because a TreeView can actually contain multiple trees, my button event handlers iterate over all the root items, calling the appropriate method.

    <collections:ArrayList x:Key="treeOfLife">
        <local:DomainViewModel Classification="Bacteria">
            …
        </local:DomainViewModel>
        <local:DomainViewModel Classification="Archaea">
            …
        </local:DomainViewModel>
        <local:DomainViewModel Classification="Eukarya">
            …
        </local:DomainViewModel>
    </collections:ArrayList>
    
    private void ExpandAll(object sender, RoutedEventArgs e)
    {
        foreach (TaxonomyViewModel item in treeView.Items)
        {
            item.ExpandAll();
        }
    }
    
    private void SelectOne(object sender, RoutedEventArgs e)
    {
        ArrayList treeOfLifeCollection = (ArrayList)this.Resources["treeOfLife"];
        TaxonomyViewModel elementToExpand = (TaxonomyViewModel)((TaxonomyViewModel)treeOfLifeCollection[2]).Subclasses[3].Subclasses[0].Subclasses[0].Subclasses[0];
    
        foreach (TaxonomyViewModel item in treeView.Items)
        {
            if (item.ExpandSuperclasses(elementToExpand))
            {
                elementToExpand.IsSelected = true;
                break;
            }
        }
    }

The CollapseAll scenario is a bit of a special case. There are really two options for collapsing all items:

  • You can set IsExpanded to false on every TreeViewItem. The CollapseAll method I showed earlier can be used in this case. If you pick this option, any previous item expansion is forgotten once you collapse all items. This means that if you expand a few items, collapse all, and expand one top level item, the previous item expansion will not be restored.

    private void CollapseAll(object sender, RoutedEventArgs e)
    {
        foreach (TaxonomyViewModel item in treeView.Items)
        {
            item.CollapseAll();
        }
    }

  • The other option is to collapse only the top level items. If you pick this option, previous expansions will be remembered and restored after collapsing all items.

    private void CollapseTopLevel(object sender, RoutedEventArgs e)
    {
        foreach (TaxonomyViewModel item in treeView.Items)
        {
            TreeViewItem tvi = treeView.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
            tvi.IsExpanded = false;
        }
    }

And last, I need to bind the TreeViewItem’s IsExpanded and IsSelected properties to the corresponding properties in my intermediate data source. At first sight, it may seem that the following XAML would work well:

    <Style TargetType="TreeViewItem">
        <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded}" />
        <Setter Property="IsSelected" Value="{Binding Path=IsSelected}" />
    </Style>

If you try this, however, you will notice that once you collapse a TreeViewItem manually by clicking on it, the Expand All button will no longer affect that item. That’s because when interacting directly with the UI, the IsExpanded property is set explicit, overwriting the binding. The solution is to make the Bindings two-way. As you would expect, two-way Bindings are not lost when the target value is set, they simply propagate the value back to the source.

    <Style TargetType="TreeViewItem">
        <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" />
        <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
    </Style>

If you have WPF installed on your machine, you can click here to see this code running as an xbap. I also link to the source at the end of this post.

Silverlight

Most of the code and XAML I showed for WPF works in Silverlight too, with the exception of the Binding in the Setter’s Value, since Silverlight currently doesn’t support that feature. In order to work around this limitation, I created custom TreeView and TreeViewItem classes that derive from the Toolkit classes and override GetContainerForItemOverride. This method is called to create each TreeViewItem container, so I was able to include the Bindings through code at the moment these containers are created.

    public class MyTreeView : TreeView
    {
        protected override DependencyObject GetContainerForItemOverride()
        {
            MyTreeViewItem tvi = new MyTreeViewItem();
            Binding expandedBinding = new Binding("IsExpanded");
            expandedBinding.Mode = BindingMode.TwoWay;
            tvi.SetBinding(MyTreeViewItem.IsExpandedProperty, expandedBinding);
            Binding selectedBinding = new Binding("IsSelected");
            selectedBinding.Mode = BindingMode.TwoWay;
            tvi.SetBinding(MyTreeViewItem.IsSelectedProperty, selectedBinding);
            return tvi;
        }
    }

Also, unfortunately we have a bug in the Toolkit TreeView that occasionally causes more than one item to appear selected. Hopefully we’ll get that fixed for the next release. (Update: this issue has been fixed!)

If you have Silverlight 2 installed, you can see a running version of this example in its own page, or embedded below:

And that’s all for today. In my next post, I will discuss a third way of expanding, collapsing and selecting TreeViewItems.

Download the WPF project (built with .NET 3.5 SP1).

Download the Silverlight project (built with Silverlight 2).


Posted by Bea under Silverlight, WPF | Comments (6)

October 29, 2008

How can I expand items in a TreeView? – Part I

Exciting!

Yesterday, the new Silverlight Toolkit (the team I now work for) was made available in CodePlex. This was announced by Scott Guthrie in his PDC keynote and by my manager Shawn Burke in his blog.

We created the Silverlight Toolkit to provide developers with a set of reusable components that maximize productivity and help take Silverlight to the next level. The toolkit is available under the Microsoft Public License, so we provide the full source code (as well as tests and samples), and encourage you to reuse our code in your applications.

If you want to know more about it, I recommend that you check out our web site, download the toolkit, and post questions in the Silverlight controls forum. My team just went through a resource-constrained, short, crazy release, where everyone on the team went beyond at so many levels, so it feels amazing to get these bits out to customers.


Expanding items in a TreeView

Among many other controls, we shipped a Silverlight TreeView, nearly identical to the WPF one. This is the first of three blog posts in which I am going to discuss different ways of expanding the items in a TreeView. These blog posts will cover both WPF and Silverlight, and point out the differences between the two. I’m hoping this will be especially useful to those of you who are familiar with one technology and interested in learning the other.

I have encountered customers in the past who had implemented complex solutions to expand all items of a TreeView at load time (which is a pretty common scenario). This scenario is actually really easy to accomplish – you can simply add an implicit Style for TreeViewItem that sets IsExpanded to true.

WPF

In WPF, this can be done by adding an implicit style to the resources of your page:

    <Page.Resources>
        <collections:ArrayList x:Key="treeOfLife">
            <local:Domain Classification="Bacteria">
                <local:Kingdom Classification="Eubacteria" />
            </local:Domain>
            …
        </collections:ArrayList>
        …
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="True" />
        </Style>
    </Page.Resources>

Couldn’t be any easier.

If you have WPF installed on your machine, you can click here to see this code running as an xbap. I also link to the source at the end of this post.

Silverlight

Now let’s look at the differences between the WPF solution and the corresponding Silverlight one.

You may have noticed that I used an ArrayList to store my data source in the WPF sample. Usually I would prefer to use a strongly typed generic collection, but for this example I chose ArrayList because the WPF XAML parser does not yet support generic types. In Silverlight, the ArrayList type is not available for customer use (it’s internal), so I used an ObjectCollection instead. ObjectCollection is a new collection type we ship in the Silverlight toolkit exactly for the purpose of defining collections in XAML. Here is its source code:

    public partial class ObjectCollection : Collection<object>
    {
        public ObjectCollection()
        {
        }
    
        public ObjectCollection(IEnumerable collection)
        {
            foreach (object obj in collection)
            {
                Add(obj);
            }
        }
    }

In this case I don’t need collection change notifications, so deriving from Collection<object> works well for me, without adding the overhead of ObservableCollection<T>. If you need collection change notifications, you can use the same technique and simply replace the base class with ObservableCollection<T>.

The Style for the TreeViewItem looks really similar to the WPF one. However, if you try this in Silverlight, you will notice that the style doesn’t get applied.

    <UserControl.Resources>
        …
        <Style TargetType="controls:TreeViewItem">
            <Setter Property="IsExpanded" Value="True" />
        </Style>
    </UserControl.Resources>

This is because Silverlight doesn’t have support for implicit styles. When using the Silverlight runtime, you need to give each style a key and refer to it explicitly using StaticResource. Because the TreeViewItems are generated automatically by the TreeView, it would be pretty hard to specify an explicit style for each one.

Fortunately, the Silverlight Toolkit contains a solution to this problem that provides a behavior similar to WPF’s implicit styles. You can use the “ImplicitStyleManager” class, and in particular its ApplyMode attached property:

    <controls:TreeView ItemsSource="{StaticResource treeOfLife}" ItemTemplate="{StaticResource treeOfLifeTemplate}" theming:ImplicitStyleManager.ApplyMode="Auto"/>

And now the implicit style is applied and the TreeView expands all items on load! If you have Silverlight 2 installed, you can see a running version of this example in its own page, or embedded below:

ImplicitStyleManager

ImplicitStyleManager only affects elements in the subtree rooted at the element where ApplyMode is set. In the sample above, implicit styles are only applied to the TreeView itself and its descendants in the visual tree (TreeViewItems, etc.). However, when searching for styles to use, ImplicitStyleManager looks within resource dictionaries in the entire name scope of the element where ApplyMode is set, as well as in the Application’s resources (just like in WPF). In this sample, it looks for implicit styles anywhere within the current UserControl (each UserControl defines a name scope), and in the Application’s resources.

You’ve already seen how to use ImplicitStyleManager in a simple scenario above, but this class provides some additional functionality. ISM (as we lovingly call it) has three different modes, which can be specified through its ApplyMode property:

Auto

ISM reapplies implicit styles every time layout is updated. So, if elements are added later to the visual tree (like the TreeViewItems in this sample), they will be implicitly styled.

Keep in mind that the LayoutUpdated event fires quite frequently (not just when elements are added to the visual tree), and walking the whole visual tree may be an expensive operation if you have a large tree. Unfortunately there is no “ItemAddedToTree” event that we could listen to, so we had to compromise on performance to offer the convenience of this mode.

In this post’s sample, the visual tree is small and very dynamic, so it makes sense to use this mode. But if your tree is relatively static or very large, you should consider using the OneTime mode instead.

OneTime

In this mode, ISM applies the implicit styles to the elements in the visual tree at load time. If new elements get added later, they will not be styled. In this blog’s sample, the generated TreeViewItems are not all instantiated at load time, so using ApplyMode=OneTime wouldn’t work.

Sometimes you may have a scenario where new items are added to the tree occasionally, but your tree is large enough that you would rather not use the Auto mode. For these situations, you can set ApplyMode to OneTime, and call ISM’s “Apply” method through code every time you want to reapply the styles. This provides extra flexibility while avoiding the performance cost of the Auto mode.

None

This mode is exactly the same as not attaching the ApplyMode property. It does not prevent styles from being propagated to the subtree where this property is set, as some people may expect. Since WPF doesn’t permit you to disable implicit styling within a subtree, ImplicitStyleManager doesn’t either.

There is more to ImplicitStyleManager than what I’ve explained here, so stay tuned for future posts.

Download the WPF project (built with .NET 3.5 SP1).

Download the Silverlight project (built with Silverlight 2).


Posted by Bea under Silverlight, WPF | Comments (7)