Click here to Skip to main content
15,886,067 members
Articles / Desktop Programming / Windows Forms
Article

Multithreaded UI Model-View Data Binding

Rate me:
Please Sign up or sign in to vote.
3.71/5 (8 votes)
8 Mar 20064 min read 52.4K   1.3K   58   3
Quick examples of the correct use of multithreaded UI data binding in Windows Forms, in .NET 2.0.

Sample Image

Introduction

This solution demonstrates a simple model-view pattern for a multithreaded Windows Forms UI in .NET 2.0. I prefer to design the "model" classes for my programs first, and then design the forms around the model. This example shows a couple of types of data binding techniques using worker threads and timers. Don't underestimate the simplicity of these examples, you can build incredibly complex user interfaces using a model based approach. Perhaps, this article will help you solve similar problems faster; I couldn't find anything on CodeProject covering this topic.

Databinding with models, threads, and timers

Note: The model class uses the System.ComponentModel.INotifyPropertyChanged interface in order to take advantage of automatic data binding.

For simplicity, a static instance of the model class is created in the Program class. Each form using the model must call the model's AddObserver method to enable background threads and timer updates.

Example 1. You can bind a control to a property of an arbitrary class. The first example shows three controls bound to the model's SliderValue property. Changing any of the controls automatically updates all three.

To set up data binding in the form:

C#
textBoxScrollValue.DataBindings.Add("Text", 
    Program.Model, "ScrollValue", false, 
    DataSourceUpdateMode.OnPropertyChanged);

trackBar1.DataBindings.Add("Value", Program.Model, 
    "ScrollValue", false, 
    DataSourceUpdateMode.OnPropertyChanged);
    
numericUpDown1.DataBindings.Add("Value", Program.Model, 
    "ScrollValue", false, 
    DataSourceUpdateMode.OnPropertyChanged);
    

int scrollValue = 0;
public int ScrollValue
{
    get { return scrollValue; }
    set
    {
        scrollValue = value;
        // Not being set from a worker thread, so 
        // UpdateObservers not required, let the 
        // default binding occur.
    }
}

Example 2. In some cases, you will need a background timer to perform periodic tasks: read hardware settings, read databases, examine system performance counters, etc. As a matter of preference, I like to abstract these operations into a model class. When the timer fires, it updates the model's data, and the model updates all of its observers. All cross-threading issues with the timer and UI threads are automatically handled in UpdateObservers.

C#
double power = 0;
public double Power
{
    get { return power; }
    set
    {
        power = value;
        UpdateObservers("Power", power);
    }
}

UpdateObservers iterates over all the model's observers and invokes a custom update delegate on each. Delegates appear to be a better choice than data binding for updating values changed via timers or background threads, because on data binding to the form, every bound control is updated. So if you have frequent thread events in the model, it can clobber the user experience in the UI. This is noticeable if you try to use a bound combo box in dropdown mode -- as the timer fires, the selection in the dropdown keeps getting reset, even if you are intending to only update some other control. For this reason, I switched to using delegates for background thread updates. If there are only one or two controls being refreshed on a timer, it's not much extra work to code.

Note: In the first version of the project, I ran into deadlock problems when trying to lock on the observer collection in AddObserver, but someone suggested iterating over an array copy instead -- this seems to work.

Lock on the observer collection array's SyncRoot property when modifying or copying the collection. This prevents multiple threads from accessing the collection at the same time.

C#
private void UpdateObservers(string propertyName, object value)
{
    Array copy;
    lock (observers.SyncRoot)
    {
        copy = observers.ToArray();
    }

    for (int n = 0; n < copy.Length; n++)
    {
        Control control = (Control)copy.GetValue(n);

        // Handle must exist.
        if (!control.IsHandleCreated)
            continue;

        if (control.IsDisposed)
            continue;

        switch (propertyName)
        {
            case "Power":
                control.Invoke(((MainForm)control).PowerDelegate, 
                                 new object[] { (double)value });
                break;

            case "StateFlag":
                control.Invoke(((MainForm)control).PowerButtonDelegate, 
                                 new object[] { (bool)value });
                break;
        }

     }

} // UpdateObservers

Example 3. Shows the use of a background thread that performs some sort of time consuming or repetitive operation -- in this case, it updates a random number in the model every 100 mSec. Cross-threading issues don't arise since all UI updates are handled by invoking a delegate in UpdateObservers. This example also shows how to control a background thread from another thread. When the timer sets the "Power" button off, the random number update thread is suspended. The "Enable" check box starts and stops the random number thread completely.

I've added an example of ComboBox data binding to the project -- this is not a totally obvious technique. I wrote two generic classes for support: ComboBoxHelper to hold values and display text, and ComboBoxBindingList to allow for data bound updates to the ComboBox's DataSource collection. Using Generics allows you to use the ComboBox for selecting friendly name items for any type of value: int, string, enum, etc. You can bind to any object collection this way, not just data sets and tables.

In the model class:

C#
public ComboBindingList<PreampEnum> preampList = 
             new ComboBindingList<PreampEnum>();

private PreampEnum preampSetting = PreampEnum.OFF;
public PreampEnum PreampSetting { 
       get { return preampSetting; } 
       set { preampSetting = value; } }

...

preampList.Add(new ComboHelper<PreampEnum>("Off", 
                                PreampEnum.OFF));
preampList.Add(new ComboHelper<PreampEnum>("Low",
                                PreampEnum.LOW));
preampList.Add(new ComboHelper<PreampEnum>("Medium", 
                             PreampEnum.MEDIUM));
preampList.Add(new ComboHelper<PreampEnum>("High", 
                               PreampEnum.HIGH));

Then set the binding in the form class:

C#
comboBox1.DataSource = Program.Model.preampList;
comboBox1.DisplayMember = "DisplayName";
comboBox1.ValueMember = "Value";
comboBox1.DataBindings.Add("SelectedValue", Program.Model, 
          "PreampSetting", false, 
          DataSourceUpdateMode.OnPropertyChanged);

For reference, if you don't want to download the source, here are the two support classes:

C#
public class ComboHelper<T>
{
    protected string displayName;
    protected T settingValue;

    public ComboHelper(string paramName, T paramValue)
    {
        displayName = paramName;
        settingValue = paramValue;
    }

    public override string ToString()
    {
        return displayName;
    }

    public string DisplayName { get { return displayName; } 
                                set { displayName = value; } }
    public T Value { get { return settingValue; } 
                     set { settingValue = value; } }
}


public class ComboBindingList<T> :  
             CollectionBase, IBindingList
{
    private ListChangedEventArgs resetEvent = new 
         ListChangedEventArgs(ListChangedType.Reset, -1);
    private ListChangedEventHandler onListChanged;

    public ComboHelper<T> this[int index] 
    {
        get 
        {
            return (ComboHelper<T>)(List[index]);
        }
        set 
        {
            List[index] = value;
        }
    }

    public int Add (ComboHelper<T> value) 
    {
        return List.Add(value);
    }

    public ComboHelper<T> AddNew() 
    {
        return (ComboHelper<T>)
               ((IBindingList)this).AddNew();
    }

    public void Remove (ComboHelper<T> value) 
    {
        List.Remove(value);
    }

    
    protected virtual void OnListChanged(ListChangedEventArgs ev) 
    {
        if (onListChanged != null) 
        {
            onListChanged(this, ev);
        }
    }
    

    protected override void OnClear() 
    {
    }

    protected override void OnClearComplete() 
    {
        OnListChanged(resetEvent);
    }

    protected override void OnInsertComplete(int index, object value) 
    {
        ComboHelper<T> c = (ComboHelper<T>)value;
        OnListChanged(new ListChangedEventArgs(
                          ListChangedType.ItemAdded, index));
    }

    protected override void OnRemoveComplete(int index, object value) 
    {
        ComboHelper<T> c = (ComboHelper<T>)value;
        OnListChanged(new ListChangedEventArgs(
                          ListChangedType.ItemDeleted, index));
    }

    protected override void OnSetComplete(int index, 
                       object oldValue, object newValue) 
    {
        if (oldValue != newValue) 
        {
            OnListChanged(new ListChangedEventArgs(
                              ListChangedType.ItemAdded, index));
        }
    }
    
    // Called by ComboHelper<T> when it changes.
    internal void ComboHelper_Changed(ComboHelper<T> cust) 
    {
        int index = List.IndexOf(cust);
        OnListChanged(new ListChangedEventArgs(
                          ListChangedType.ItemChanged, index));
    }
    

    // Implements IBindingList.
    bool IBindingList.AllowEdit 
    { 
        get { return true ; }
    }

    bool IBindingList.AllowNew 
    { 
        get { return true ; }
    }

    bool IBindingList.AllowRemove 
    { 
        get { return true ; }
    }

    bool IBindingList.SupportsChangeNotification 
    { 
        get { return true ; }
    }
    
    bool IBindingList.SupportsSearching 
    { 
        get { return false ; }
    }

    bool IBindingList.SupportsSorting 
    { 
        get { return false ; }
    }


    // Events.
    public event ListChangedEventHandler ListChanged 
    {
        add 
        {
            onListChanged += value;
        }
        remove 
        {
            onListChanged -= value;
        }
    }

    // Methods.
    object IBindingList.AddNew() 
    {
        ComboHelper<T> c = new ComboHelper<T>("", 
                                     (T)new object());
        List.Add(c);
        return c;
    }


    // Unsupported properties.
    bool IBindingList.IsSorted 
    { 
        get { throw new NotSupportedException(); }
    }

    ListSortDirection IBindingList.SortDirection 
    { 
        get { throw new NotSupportedException(); }
    }


    PropertyDescriptor IBindingList.SortProperty 
    { 
        get { throw new NotSupportedException(); }
    }


    // Unsupported Methods.
    void IBindingList.AddIndex(PropertyDescriptor property) 
    {
        throw new NotSupportedException(); 
    }

    void IBindingList.ApplySort(PropertyDescriptor property, 
                                ListSortDirection direction) 
    {
        throw new NotSupportedException(); 
    }

    int IBindingList.Find(PropertyDescriptor property, object key) 
    {
        throw new NotSupportedException(); 
    }

    void IBindingList.RemoveIndex(PropertyDescriptor property) 
    {
        throw new NotSupportedException(); 
    }

    void IBindingList.RemoveSort() 
    {
        throw new NotSupportedException(); 
    }

}

Using the code

Build it and run it. Move the TrackBar control back and forth. Open additional observer forms and watch data updates on all open forms.

Points of Interest

Visual Studio 2005 helps you eliminate "cross-threading" errors in your code. In debug builds, you'll get cross-threading exceptions whenever it detects a threading error. In release builds, this exception is disabled.

History

  • First draft: February 17, 2006.
  • Updated code for multiple observers: February 19, 2006.
  • Improved binding, added ComboBox binding, incorporated suggestions from CMJobson: March 6, 2006.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralCross-Threading Error Pin
Christopher Pearce9-Mar-06 23:15
Christopher Pearce9-Mar-06 23:15 
GeneralGetting round the deadlock problem Pin
CMJobson6-Mar-06 10:58
CMJobson6-Mar-06 10:58 
GeneralRe: Getting round the deadlock problem Pin
Paul Shaffer6-Mar-06 13:02
Paul Shaffer6-Mar-06 13:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.