Cascading ItemsSource Bindings in Silverlight

by Administrator 15. March 2010 21:35

When building a complex data visualization application in Silverlight, I discovered a disturbing issue when dealing with multiple DataGrids on the same screen.  If the ItemsSource of a particular DataGrid gets updated as the result of a SelectedItem change in another DataGrid the child grid would not display any items.  Worse, adding code to query the ItemsSource of the child grid shows that does have a valid and correct ItemsSource, so why doesn’t it display?  In this article we’ll quickly demonstrate the problem and show a solution.

Sample App

I’ve created a simple example with a ‘parent’ DataGrid containing Orders and a ‘child’ DataGrid meant to display the Order Details of the selected Order.  Throw in a small amount of sample data and we wind up with this:

dbf0

You can see that we have selected the first and only row in the top DataGrid and it has a valid OrderDetails property.  Clicking on the Check Items Source button reveals that something should be shown in the bottom DataGrid:

dbf1

So what’s going on?  Let’s take a look at the XAML and data binding code to see if anything is amiss.

Data Binding Code

I have created a simple ViewModel for this screen, with code as follows:

public class ScreenModel : BindableType
{
    public ScreenModel()
    {
        Orders = new ObservableCollection<Order>();
        Orders.Add(new Order());
    }


    private ObservableCollection<Order> _Orders;

    public ObservableCollection<Order> Orders
    {
        get { return _Orders; }
        set
        {
            _Orders = value;
            OnPropertyChanged("Orders");
        }
    }

    private Order _SelectedOrder;

    public Order SelectedOrder
    {
        get { return _SelectedOrder; }
        set
        {
            _SelectedOrder = value;
            //Doesn't work:
            OnPropertyChanged("SelectedOrder");

BindableType implements the basic INotifyPropertyChanged functionality.  Hopefully you agree that this is extremely vanilla code, and that it’s a bit odd that this does not work.  Maybe something is wrong with the XAML?

            <TextBlock>Master Grid</TextBlock>
            <my:DataGrid x:Name="MasterGrid" MinHeight="100"
                         ItemsSource="{Binding Orders}" SelectedItem="{Binding SelectedOrder, Mode=TwoWay}" />
            <TextBlock>Detail Grid</TextBlock>
            <my:DataGrid x:Name="DetailGrid" MinHeight="100" ItemsSource="{Binding SelectedOrder.OrderDetails}" />
            <Button x:Name="ChkBtn" Content="Check Items Source" Click="ChkBtn_Click"></Button>

The XAML is extremely basic as well.  How can you fix this?  Searching through forums it seems that when Silverlight 3 was in BETA changing the bottom DataGrid to have a TwoWay binding to its ItemsSource would fix this but I didn’t have luck with this.

Problem Solution

By groping in the dark and changing a single line of code I was able to get the correct behavior:

dbf2

The only code change is this:

private Order _SelectedOrder;

public Order SelectedOrder
{
    get { return _SelectedOrder; }
    set
    {
        _SelectedOrder = value;
        //Doesn't work:
        //OnPropertyChanged("SelectedOrder");

        //Works:
        Deployment.Current.Dispatcher.BeginInvoke(() => OnPropertyChanged("SelectedOrder"));
    }
}

All I’ve done here is to invoke the Property Change Notification for SelectedOrder back onto a Dispatcher thread.  Why is this necessary?  I can’t say for sure.  My guess is that something down in the bowels of Silverlight is attempting to detect some sort of layout cycle or infinite recursion here.  If you find this situating in your code this should work for you.

Tags:

AddRange for ObservableCollection in Silverlight 3

by Administrator 4. March 2010 01:43

It’s great that the built-in controls in Silverlight understand INotifyCollectionChanged, and that ObservableCollection<T> ships with Silverlight.  However, when adding large data sets to ObservableCollection<T> the performance  can be brutal due to the fact that ObservableCollection<T> fires an event with every operation on the collection.  Most of the time this is what you want however some bulk operations like AddRange would be nice.  Let’s take a look at a way to add AddRange() to ObservbleCollection<T> using a technique that vastly increases performance.

Example UserControl

We’ll use a simple Silverlight root visual with a DataGrid on it to demonstrate this technique. Note that we are using a new class called SmartCollection<T> and an Item class with some randomly generated field values.

namespace HandWaver.AG.OCAddRange
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            _collection = new SmartCollection<Item>();
            DasGrid.ItemsSource = _collection;            
        }

        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            Dispatcher.BeginInvoke(() => BuildItems());
        }

        TimeSpan Time(Action a)
        {
            var start = DateTime.Now;
            a();
            var end = DateTime.Now;
            return end - start;
        }

        SmartCollection<Item> _collection;
        
        private void BuildItems()
        {
            Action slow = () =>
            {
                for (int i = 0; i < 25000; ++i)
                {
                    _collection.Add(new Item());
                }
            };

Using this technique and displaying the time it took to add these items to the data grid gives me this result.  Note that Stopwatch is not present in Silverlight 3.

time0

Nearly 10 seconds to add 25,000 items to the DataGrid even though most of them are not visible.  Let’s see what SmartCollection<T> can do with slightly different code:

Action fast = () =>
{                
    var sourceList = new List<Item>();
    for (int i = 0; i < 25000; ++i)
    {
        sourceList.Add(new Item());
    }
    
    _collection.AddRange(sourceList);                
};

var time =
    //Time(slow);
    Time(fast);

So we’ve built a source collection and used the AddRange implementation.  How does it do?

time1

That’s a pretty significant speed up! Let’s go ahead and look at the code for SmartCollection<T> now.

using System;
using System.Linq;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace HandWaver.AG.PresentationModel
{
    public class SmartCollection<T> : ObservableCollection<T>
    {
        public SmartCollection()
            : base()
        {
            _suspendCollectionChangeNotification = false;
        }


        bool _suspendCollectionChangeNotification;

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (!_suspendCollectionChangeNotification)
            {
                base.OnCollectionChanged(e);
            }
        }

        public void SuspendCollectionChangeNotification()
        {
            _suspendCollectionChangeNotification = true;
        }

        public void ResumeCollectionChangeNotification()
        {
            _suspendCollectionChangeNotification = false;
        }


        public void AddRange(IEnumerable<T> items)
        {
            this.SuspendCollectionChangeNotification();
            int index = base.Count;
            try
            {
                foreach (var i in items)
                {
                    base.InsertItem(base.Count, i);
                }
            }
            finally
            {
                this.ResumeCollectionChangeNotification();
                var arg = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
                this.OnCollectionChanged(arg);
            }
        }

    }

}

Note that we could do an AddRange implementation via an extension method but we would not gain the performance improvements since events would still fire for every operation.  By extending ObservableCollection<T> we gain the ability to create our own AddRange method.  The AddRange method sets a flag that temporarily suppresses the collection changed notification for ObservableCollection<T>.   At the end it uses NotifyCollectionChangedAction.Reset to indicate that a significant change was made to the collection.  The UI rebuilds itself in a fraction of the time it took to add the items one by one.  You can use this technique to gain huge performance improvements for your Silverlight 3 apps dealing with large data sets.

Tags:

Speaking at FVNUG Day of .Net

by Administrator 2. March 2010 02:40

I have the privilege for the second year in a row of speaking at the Fox Valley Day of .NET.  Last year the event was a little later in the year (or MIX was earlier?) and I gave the keynote as part of the MIX It Up! tour.  This year I am actually going to MIX so maybe I’ll have to come back.

I will be giving the MEF talk I mentioned not too long ago and while I will have some good Silverlight content this is more about MEF itself.  The guys up there are gracious hosts and I am looking forward to getting up there and sharing one of my favorite new technologies.

If MEF is not your thing, please consider seeing my colleague Christopher Barwick’s talk on Business Intelligence: “BI: from Database to Warehouse  to OLAP to Excel with SQL Server 2008 & Excel 2007” running in the same time slot as my presentation.  Chris has really been stepping up his involvement in the community and he has a lot to share.

For my part I will be watching Dave Bost talk about Silverlight 4 right after my talk.  While I imagine I’ve already consumed this content I can’t get enough Silverlight and Dave is a good presenter so I’ll be sitting in.

Please check out their site and come see us!  They have a lot of swag this year so your chances of coming home with something extremely useful are very good.

Tags:

About the author

Damon Payne is a Microsoft MVP specializing in Smart Client solution architecture. 

INETA Community Speakers Program

Month List

Page List

flickr photostream