65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.71/5 (8投票s)

2006年2月20日

4分钟阅读

viewsIcon

53273

downloadIcon

1303

.NET 2.0 中多线程 UI 数据绑定的正确用法快速示例。

Sample Image

引言

该解决方案演示了 .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 用于允许对 ComboBoxDataSource 集合进行数据绑定更新。使用泛型允许你将 ComboBox 用于选择任何类型值的友好名称项:intstringenum 等。你可以这样绑定到任何对象集合,而不仅仅是数据集和表。

在模型类中

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 日。
© . All rights reserved.