Thursday, July 23, 2009

DataGrid in Silverlight and Sorting/Paging: DataContext Changed

I ran into an interesting bug today. I have a grid with a few custom controls. For example, we have a battery icon that indicates how much battery life a mobile device has remaining. The custom control takes the percentage of "life" left in the battery and creates a visual image to show in the grid.

The problem was that as I was paging and sorting the grid, I quickly realized that the images weren't changing. They rendered the very first time just fine, but after that it was the same set of images, regardless of whether I paged or sorted or not. The only way to get a fresh set was by refreshing the grid.

After some research, I found the issue. For performance reasons, the grid will create the control for each row and then reuse that control. For example, if I have 15 rows in my grid, Silverlight will generate 15 controls. However, when I sort or page, instead of creating 15 new controls, Silverlight simply updates the binding to the existing controls.

The problem is that there is no easy event to notify my control that the DataContext changed! I was plugging into the Loaded event, which only was happening on the first rendering. After doing some research online, I finally found the solution, which requires three steps.

Step One — Create a dependency property

We are going to create our own dependency property simply to know when the data context changes. It's a "dummy" property that gets tied to the DataContext itself. The snippet looks like this (in this case, my control is called ControlName). Note that I am asking it to raise an event called _DataContextChanged when the property is updated, and that I don't actually create a "InternalDataContext" property anywhere.

public static readonly DependencyProperty InternalDataContextProperty =
            DependencyProperty.Register("InternalDataContext",
                                        typeof (Object),
                                        typeof (ControlName),
                                        new PropertyMetadata(_DataContextChanged));

Step Two — Bind the property to DataContext

This ensures our dependency property is wired in to call our custom event. In the constructor, simply invoke:

SetBinding(InternalDataContextProperty, new Binding());

Now we've got the glue in place.

Step Three — act on the change!

I created the event, _DataContextChanged, to respond to any updates:

private static void _DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    ControlName control = (ControlName) sender;
    if (e.NewValue != null)
    {
        ControlName.DoSomething(e.NewValue);
    }
}

As you can see, the sender is my control, so I cast it back. Then, if there actually is a new value, I do something with it. Note that you can also get the old value and perform actions against that as well. That's it - now my control is "aware" when the grid changes and can behave appropriately.

Jeremy Likness