ILoggable

A place to keep my thoughts on programming

July 10, 2007 .net , , ,

A generic asynchronous INotifyPropertyChanged helper

The other day I was lamenting what I consider to be an omission in Data-binding’s responsibilities, i.e. the lack of invocation of an INotifyPropertyChanged PropertyChanged event on the appropriate UI thread. Instead, INotifyPropertyChanged implementors are expected to be updated only on the UI thread in order to work.

I figured I should be able to throw a proxy between the actual INotifyPropertyChanged implementor and the binding, and have this proxy be instantiated with awareness of the observing form. Thus the proxy could do the invocation on the UI thread and still let me use INotifyPropertyChanged without creating a dependency on knowing that the object was being observed by a UI element. That line of thought lead to worked as a static proxy, that I had to create by hand. Once I switched to Castle‘s Dynamic Proxy, it didn’t work anymore, the event somehow being swallowed along the way.

I also considered looking at creating my own BindingContext but that didn’t bear fruit either. It was then that I realized that I didn’t need to create a proxy that matched my source object, since data binding didn’t care what it bound to, as long as it had the dataMember it was being told about. I started down a path of creating a wrapper class using Reflection.Emit when I further reasoned that even the dataMember that’s exposed doesn’t have to match the one of the data source. This opened the door to just using dynamic property invocation and a simple generic helper class emerged:

using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Windows.Forms;

namespace FullMotion.Helpers
{
  /// <summary>
  /// A helper class for creating a binding on an object that may be changed
  /// asynchronously from the bound UI thread.
  /// </summary>
  public class AsyncBindingHelper : INotifyPropertyChanged
  {
    /// <summary>
    /// Get a binding instance that can invoke a property change
    /// on the UI thread, regardless of the originating thread
    /// </summary>
    /// <param name="bindingControl">The UI control this binding is added to</param>
    /// <param name="propertyName">The property on the UI control to bind to</param>
    /// <param name="bindingSource">The source INotifyPropertyChanged to be
    /// observed for changes</param>
    /// <param name="dataMember">The property on the source to watch</param>
    /// <returns></returns>
    public static Binding GetBinding( Control bindingControl,
                                      string propertyName,
                                      INotifyPropertyChanged bindingSource,
                                      string dataMember )
    {
      AsyncBindingHelper helper 
        = new AsyncBindingHelper(bindingControl,bindingSource,dataMember);
      return new Binding(propertyName,helper,"Value");
    }

    Control bindingControl;
    INotifyPropertyChanged bindingSource;
    string dataMember;

    private AsyncBindingHelper( Control bindingControl,
                                INotifyPropertyChanged bindingSource, 
                                string dataMember )
    {
      this.bindingControl = bindingControl;
      this.bindingSource = bindingSource;
      this.dataMember = dataMember;
      bindingSource.PropertyChanged 
        += new PropertyChangedEventHandler(bindingSource_PropertyChanged);
    }

    void bindingSource_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      if (PropertyChanged != null && e.PropertyName == dataMember)
      {
        if (bindingControl.InvokeRequired)
        {
          bindingControl.BeginInvoke(
            new PropertyChangedEventHandler(bindingSource_PropertyChanged),
            sender,
            e );
          return;
        }
        PropertyChanged(this, new PropertyChangedEventArgs("Value"));
      }
    }

    /// <summary>
    /// The current value of the data sources' datamember
    /// </summary>
    public object Value
    {
      get
      {
        return bindingSource.GetType().GetProperty(dataMember)
          .GetValue(bindingSource,null);
      }
    }
    #region INotifyPropertyChanged Members
    /// <summary>
    /// Event fired when the dataMember property on the data source is changed
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    #endregion
  }
}

To use this class, simply replace this:

myLabel.DataBindings.Add("Text", myDataSource,"MyProperty");

with this:

myLabel.DataBindings.Add(
  AsyncBindingHelper.GetBinding(myLabel, "Text", myDataSource,"MyProperty") );

The one tricky thing about this is that it may cause uncollectable references. We’re creating an instance of AsyncBindingHelper behind the scenes that sets up an event handler on our binding source. Which means we can’t get rid of that event subscription. Theoretically, once the binding control goes out of scope, the helper should also go out of scope, relinquishing the reference it holds on the binding source.

The other thing that would be simple to do is to turn this helper into an Extension Method on INotifyPropertyChanged, once I move my code over to Orcas. That way creating an asynchronous binding on an instance of INotifyPropertyChanged would become even cleaner and straight forward.

6 to “A generic asynchronous INotifyPropertyChanged helper”

  1. Kyle says...

    Thank you SO MUCH!
    Solved all my problems.

  2. Pawel says...

    Fantastic solution! It helps me a lot. Thank you for publishing this.

  3. Andy says...

    Fantastic! You sir, are a genius.

  4. Andy says...

    I now realize that this breaks two way binding. I thought that the following might work, but it fails to set the property on the data object, and stops focus from leaving the textbox:
                set
                {
                    if (Value != value)
                    {
                        bindingSource.GetType().GetProperty(dataMember).SetValue(bindingSource, value, null);
                    }
                }
     

  5. Steven Wilber says...

    Thanks for a really elegant solution. I've been messing around with BeginInvoke etc.
    I'm only using one way binding, so the issue you raise is not a problem.
    Brilliant. Thanks.

  6. Wolfgang Bruckner says...

    Add setter for 2Way-binding to work:

    public object Value
    {
    get
    {
    return bindingSource.GetType().GetProperty(dataMember)
    .GetValue(bindingSource, null);
    }
    set
    {
    bindingSource.GetType().GetProperty(dataMember).SetValue(bindingSource, value, null);
    }
    }

Leave a comment