多线程 UI 模型-视图数据绑定






3.71/5 (8投票s)
2006年2月20日
4分钟阅读

53273

1303
.NET 2.0 中多线程 UI 数据绑定的正确用法快速示例。
引言
该解决方案演示了 .NET 2.0 中多线程 Windows Forms UI 的简单模型-视图模式。我倾向于先设计程序的“模型”类,然后再围绕模型设计窗体。此示例展示了几种使用工作线程和计时器的数据绑定技术。不要低估这些示例的简单性,你可以使用基于模型的方法构建极其复杂的用户界面。或许,本文将帮助你更快地解决类似问题;我在 CodeProject 上找不到任何涵盖此主题的内容。
使用模型、线程和计时器进行数据绑定
注意:模型类使用了 System.ComponentModel.INotifyPropertyChanged
接口,以便利用自动数据绑定。
为了简单起见,模型类的一个静态实例在 Program
类中创建。使用模型的每个窗体都必须调用模型的 AddObserver
方法才能启用后台线程和计时器更新。
示例 1。 你可以将控件绑定到任意类的属性。第一个示例显示了三个绑定到模型 SliderValue
属性的控件。更改任何一个控件都会自动更新所有三个。
在窗体中设置数据绑定
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.
}
}
示例 2。 在某些情况下,你需要一个后台计时器来执行周期性任务:读取硬件设置、读取数据库、检查系统性能计数器等。作为一种偏好,我喜欢将这些操作抽象到一个模型类中。当计时器触发时,它会更新模型的数据,模型会更新其所有观察者。计时器和 UI 线程的所有跨线程问题都在 UpdateObservers
中自动处理。
double power = 0;
public double Power
{
get { return power; }
set
{
power = value;
UpdateObservers("Power", power);
}
}
UpdateObservers
遍历模型的所有观察者,并在每个观察者上调用自定义更新委托。对于通过计时器或后台线程更改的值的更新,委托似乎比数据绑定更好,因为当数据绑定到窗体时,每个绑定的控件都会被更新。因此,如果模型中频繁发生线程事件,可能会破坏 UI 中的用户体验。如果尝试在下拉模式下使用绑定的组合框,这一点会很明显——当计时器触发时,下拉框中的选择会不断重置,即使你只想更新其他控件。因此,我改用了委托来处理后台线程更新。如果只有一个或两个控件通过计时器刷新,那么编码工作量也不会增加多少。
注意:在项目的第一个版本中,我在 AddObserver
中锁定观察者集合时遇到了死锁问题,但有人建议迭代数组副本而不是——这似乎可行。
在修改或复制集合时,锁定观察者集合数组的 SyncRoot
属性。这可以防止多个线程同时访问集合。
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
示例 3。 展示了后台线程的使用,该线程执行一些耗时或重复的操作——在本例中,它每 100 毫秒更新模型中的一个随机数。不会出现跨线程问题,因为所有 UI 更新都通过调用 UpdateObservers
中的委托来处理。此示例还展示了如何从另一个线程控制后台线程。当计时器关闭“Power”按钮时,随机数更新线程会暂停。“Enable”复选框会完全启动和停止随机数线程。
我在项目中添加了一个 ComboBox
数据绑定的示例——这不是一个完全显而易见的技术。我编写了两个泛型类来支持:ComboBoxHelper
用于保存值和显示文本,ComboBoxBindingList
用于允许对 ComboBox
的 DataSource
集合进行数据绑定更新。使用泛型允许你将 ComboBox
用于选择任何类型值的友好名称项:int
、string
、enum
等。你可以这样绑定到任何对象集合,而不仅仅是数据集和表。
在模型类中
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));
然后在窗体类中设置绑定
comboBox1.DataSource = Program.Model.preampList;
comboBox1.DisplayMember = "DisplayName";
comboBox1.ValueMember = "Value";
comboBox1.DataBindings.Add("SelectedValue", Program.Model,
"PreampSetting", false,
DataSourceUpdateMode.OnPropertyChanged);
供参考,如果你不想下载源代码,这里是两个支持类
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();
}
}
使用代码
构建并运行它。前后移动 TrackBar
控件。打开额外的观察者窗体,并观察所有打开窗体上的数据更新。
关注点
Visual Studio 2005 可以帮助你消除代码中的“跨线程”错误。在调试构建中,每当检测到线程错误时,你都会收到跨线程异常。在发布构建中,此异常被禁用。
历史
- 初稿:2006 年 2 月 17 日。
- 更新了多观察者代码:2006 年 2 月 19 日。
- 改进了绑定,添加了
ComboBox
绑定,并结合了 CMJobson 的建议:2006 年 3 月 6 日。