Tuesday, July 28, 2009

Silverlight DataContext Changed Event

One known issue with Silverlight is that the DataContext bound to a control may change, but there is no readily available change event. Unlike WPF, you don't have an explicit event to register with in order to track changes. This becomes a problem in controls like the DataGrid control which reuses the same control instances for each page. Even though fresh data is bound, if your control isn't aware that the data context changed, it will keep stale content.

If you search online you'll find the solution is simple: you create a dependency property that is actually based on the data context (call it a "dummy" property) and then register for changes to that property. I was glad to find the solution but wanted something a little more reusable (remember, I like the DRY principle: don't repeat yourself, so when I find myself writing the same line of code more than once I have to go back and refactor).

The solution? I was able to find something that I think works well and involves an interface and a static class.

First, I want to identify when a control should be aware of changes to DataContext and also provide a method to call when this happens. That was easy enough. I created IDataContextChangedHandler and defined it like this:

public interface IDataContextChangedHandler<T> where T: FrameworkElement 
{
   void DataContextChanged(T sender, DependencyPropertyChangedEventArgs e);
}

As you can see, it is a simple interface. A method is called with the sender (which will presumably be the control itself) and the arguments for a dependency property changed event. It is typed to T, of course.

Next, I used generics to create a base class that manages the "fake" dependency property:

public static class DataContextChangedHelper<T> where T: FrameworkElement, IDataContextChangedHandler<T>
{
    private const string INTERNAL_CONTEXT = "InternalDataContext"; 

    public static readonly DependencyProperty InternalDataContextProperty =
        DependencyProperty.Register(INTERNAL_CONTEXT,
                                    typeof(Object),
                                    typeof(T),
                                    new PropertyMetadata(_DataContextChanged));

    private static void _DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        T control = (T)sender;
        control.DataContextChanged(control, e);
    }

    public static void Bind(T control)
    {
        control.SetBinding(InternalDataContextProperty, new Binding());
    }
}

As you can see, the class does a few things and works for any framework element, which is a "basic building block" that supports binding. It is typed to the FrameworkElement but also requires that the target implements IDataContextChangedHandler. It creates a dependency property. Because the data context can be any object, the type of the dependency is object, but the type of the parent is the framework element itself ("T"). When something happens to the property, it will invoke _DataContextChanged.

The event handler is sent the control that raised the event as well as the arguments for the old and new properties in the data context. We simply cast the sender back to its original type of T. Then, because we know it implements IDataContextChangedHandler, we can simply call DataContextChanged.

Finally, there is a static call to bind the control itself.

Now let's put the pieces together. Let's say you have a control that makes a gauge based on a data value, and you want to put the control in the grid. You need to know when the DataContext changes, because you will update your gauge. The control will look like this:

public partial class Gauge : IDataContextChangedHandler<Gauge> 
{
   public Gauge() 
   {
      InitializeComponent();
      DataContextChangedHelper<Gauge>.Bind(this); 
   }

   public void DataContextChanged(Gauge sender, DependencyPropertyChangedEventArgs e)
   {
      if (e.NewValue != null)
      {
         int gaugeLevel = (int)e.NewLevel;
         _UpdateImage(gaugeLevel);
      } 
   }
}

And there you have it - to register for the data context changing, we simply implemented IDataContextChangedHandler and then registered by calling Bind in our constructor.

Jeremy Likness

9 comments:

  1. Jeremy,

    Great post here. I searched around the web for suggestions on ways to solve this very obvious problem with Silverlight and I think yours was the best I encountered.

    Thanks for the hard work!

    -Casey

    ReplyDelete
  2. is the same solution using SL4??

    ReplyDelete
  3. Hi
    Great article, i have abit trouble understanding how all this works, im quite new to silverlight.

    is it correct to assume that the
    control.SetBinding(DummyProperty, new Binding())

    because the binding doesnt have a binding path or source it will listen for its inherited datacontext data to change?

    i have a hard time understanding it hope you can clear things up for me

    Thanks again for a great article!

    ReplyDelete
  4. Please explain what does the line do:
    control.SetBinding(InternalDataContextProperty, new Binding());

    What is bound to what?

    Thanks!

    ReplyDelete
  5. Sure. It's bound to ... nothing. We create an empty binding. This allows that property to participate in the data-binding hierarchy. Even though the binding attaches to nothing, whenever the data context changes, it will fire an update. This is what lets us tap into that update and react to the change.

    ReplyDelete
  6. Ok, InternalDataContextProperty is bound to nothing because there is no Source set. So, it appears that every DependencyProperty that is bound to any control as a target will fire its "_DataContextChanged " event when control.DataContext is changed, right? But why it works in this way? _DataContextChanged event of the dependency property must fire only the dependency property is changed. What is changed if it has no source? I tried to set DataContext as List and in this case in _DataContextChanged method e.NewValue = List.Count. When I tried to set DataContext as string("any text") then e.NewValue = "any text". So, I see that the source of the dependency property is established even though it is not set directly but I can't realize how it decides what will be the source.

    I've read some articles about DependencyProperty, data binding in Silverlight and some documentation too, but I can't remember some explanation or examples that can explain this. And it's intriguing me. I mean you had known about this specific mechanism and its behavior before you invented this solution, could you tell me where can I read about it too?

    ReplyDelete
  7. I can't tell you exactly where I learned it, but it's a key thing to know: how a particular dependency property gets its value.

    Take a look here for orders of precedence:

    http://msdn.microsoft.com/en-us/library/ms743230.aspx

    The other piece is simply that dependency properties are hierarchical so if I don't explicitly set a value, it will inherit from a parent higher up the chain. This is useful because any binding will then fire if the data context anywhere on the chain changes, because it would have to resolve to a new value.

    ReplyDelete
  8. Oh, I think I understand now, but it is not about dependency property precedence. It's just binding mech. makes Binding.Source=control.DataContext by default.

    ReplyDelete
  9. I have created a more generic approach here: http://www.pochet.net/blog/2010/06/16/silverlight-datacontext-changed-event-and-trigger/

    Emiel

    ReplyDelete