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:
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;
}
}
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
.
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.
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);
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;
}
}
}
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:
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:
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:
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));
}
}
internal void ComboHelper_Changed(ComboHelper<T> cust)
{
int index = List.IndexOf(cust);
OnListChanged(new ListChangedEventArgs(
ListChangedType.ItemChanged, index));
}
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 ; }
}
public event ListChangedEventHandler ListChanged
{
add
{
onListChanged += value;
}
remove
{
onListChanged -= value;
}
}
object IBindingList.AddNew()
{
ComboHelper<T> c = new ComboHelper<T>("",
(T)new object());
List.Add(c);
return c;
}
bool IBindingList.IsSorted
{
get { throw new NotSupportedException(); }
}
ListSortDirection IBindingList.SortDirection
{
get { throw new NotSupportedException(); }
}
PropertyDescriptor IBindingList.SortProperty
{
get { throw new NotSupportedException(); }
}
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.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.