Monday 9 May 2011

Observable Collection, second look.

Observable Collection by definition provide notification when an item is added, removed or refreshed. It is pretty cool nifty item to have. So if you want a type of collection that automatically notifies the UI to update when underlying data changes, it has to be Observable Collection. Less code, more feature out of the box. But I want to point out two things when using Observable Collection.

Most of the people forget the very import thing, if you are using Observable collection then for every change, Observable Collection fires an event. If it is very small numbers then you will not notice the performance issue. But if for some reason you are adding and/or removing 100s or 1000s of rows then you will see visible performance degradation. If your data is flat or if it does not have a collection inside the collection then there should not be any performance hit either. I wrote a sample code to time it so that I  can document the performance issue if any.

Before we go into the example, I would like to point out the second issue of Memory Leak when using Observable Collection. Once you initialize a bound observable collection, do not new up again, rather, clear the collection and then add new items to the same collection. You can read about it in here and also here.

Now lets look at the performance timings.

Scenario 1: Simple observable collection class.

My backend data model is PERSON class

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Now the XAML

<Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="80"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid HorizontalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Button Name="Add" Content="Add" Click="Add_Click" Grid.Column="0" HorizontalAlignment="Center" Margin="4,4,4,4"/>
            <Button Name="Clear" Content="Clear" Click="Clear_Click" Grid.Column="1" HorizontalAlignment="Center" Margin="4,4,4,4"/>
            <TextBox Name="Numbers" Grid.Column="2" Width="100"/>
        </Grid>
        <c1:C1FlexGrid Name="_grid" Grid.Row="1" />
    </Grid>

The top part has two buttons, one to add rows and another one to remove rows from the collection. The third is a textbox, where you can enter how many rows you want to add or remove. What we are currently interested in is, adding rows to the grid on the fly.

private void Add_Click(object sender, RoutedEventArgs e)
{
     DateTime dt = DateTime.Now;
     for(int i=0;i<int.Parse(Numbers.Text);i++)
        _people.Add(new Person() { Age = i + 10, Name = "Name" + i.ToString() });
     DateTime dt1 = DateTime.Now;
     TimeSpan dt2 = dt1.Subtract(dt);
      Debug.WriteLine(string.Format("{0}:{1}:{2}:{3}", dt2.Hours, dt2.Minutes, dt2.Seconds, dt2.Milliseconds));
}

Armed with above code and XAML if you would run, the time it takes to add 1000 new object to the collection is 107ms. Not bad, no performance hit.

Scenario 2: Hierarchical Collection using Observable Collection.

We modify the Person Class as follows to include a children of same type.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public ObservableCollection<Person> Children { get; set; }
    public Person()
    {
       Children = new ObservableCollection<Person>();
    }
}

With this change, if you would run the same code, it takes 10.263s. You can see why the time jumped by 10 seconds, if I would remove the constructor from the code, it still comes down to 9.623ms. So the constructor is not the problem here.Lets move on to the third scenario.

Scenario 3: Hierarchical Collection using Custom Class derived off of Observable collection.

This is an interesting scenario that I did not think about it till I ran into a performance issue with grid. The idea behind this scenario is that, since by definition, Observable Collection fires an event notification for any change to the data, in our case, when we are adding a bunch of data, we do not want to fire event for each and individual change rather, fire an event at the end of all the changes are made. So the solution is to derive a new class off of Observable Collection and make sure you turn off the notification before adding range and at the end turn the notification on. I followed the Smart Collection explained in Daamir blog. Just in case, I added the class definition right here as well.

public class SmartCollection<T> : ObservableCollection<T>
    {
        public SmartCollection()
            : base()
        {
            _suspendCollectionChangeNotification = false;
        }
        public SmartCollection(List<T> list) : base(list) { }
        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);
            }
        }
        public void Repopulate(IEnumerable<T> items)
        {
            this.Clear();
            this.AddRange(items);
        }
    }
If you look at the code, three piece of information interesting to current article. SuspendCollectionChangeNotification, which is to turn off the notification and ResumeCollectionChangeNotification to turn on the notification. These two methods need to be invoked if you are adding one row at a time through your code. On the other hand, if you are adding a collection to the Observable Collection, then call AddRange method, which internally will take care of turning on and off the notification. Now lets look at the code change
public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public SmartCollection<Person> Children { get; set; }
        public Person()
        {
            Children = new SmartCollection<Person>();
        }
    }

Modify the code bind and change all Observable Collection Reference to SmartCollection. Also change the for loop where we add individual items to SmartCollection to List as follows

private void Add_Click(object sender, RoutedEventArgs e)
        {
            DateTime dt = DateTime.Now;
            Debug.WriteLine(string.Format("{0}:{1}:{2}:{3}", dt.Hour, dt.Minute, dt.Second, dt.Millisecond));
            List<Person> ppl = new List<Person>();
            for(int i=0;i<int.Parse(Numbers.Text);i++)
                ppl.Add(new Person() { Age = i + 10, Name = "Name" + i.ToString() });
            _people.AddRange(ppl);
            DateTime dt1 = DateTime.Now;
            TimeSpan dt2 = dt1.Subtract(dt);
            Debug.WriteLine(string.Format("{0}:{1}:{2}:{3}", dt2.Hours, dt2.Minutes, dt2.Seconds, dt2.Milliseconds));
        }

With these changes if you would run the program, you will see the 1000 row insertion only took 31ms.  Now the question is, why can’t we do the step as creating temporary collection and then assign it back to observable collection? Wouldn’t it work the same way? One approach would be

var concatList = _people.Concat(pp);

_people = new ObservableCollection(concatList)

in my opinion, newing Observable collection had some memory problem in Silverlight. The recommendation was always new up only once and from there on, if you want to add items to it, you add item to it or clear and then add item to it. That is the only reason I did not try it out.

So the bottom line is that, when we use Observable Collection take care not to fire event when you are working on too large of data. As I mentioned earlier, if your data is flat then you will not incur any performance problems but if you have hierarchical data then I would recommend you to switch to a model to turn on and off the notification model.

If any of you have good way to do it, please feel free to drop me a note. I am very much interested in learning and understand new ways of doing things.

0 comments:

Post a Comment