WinForms多线程数据绑定场景,最佳实践?

时间:2009-03-02 15:25:28

标签: winforms multithreading data-binding

我目前正在设计/重新编写应用程序的数据绑定部分,该应用程序大量使用winforms数据绑定和来自后台线程的更新(每秒一次> 100条记录)。

假设应用程序是股票交易应用程序,后台线程监视数据更改并将它们放到数据对象上。这些对象存储在BindingList<>中并实现INotifyPropertyChanged以通过数据绑定将更改传播到winforms控件。 此外,数据对象当前正在通过WinformsSynchronizationContext.Send将更改编组到UI线程。 用户可以在UI中输入一些值,这意味着可以从两侧更改某些值。并且更新不会超出用户值。

所以我想到了几个问题:

  • 是否有一般设计 - 指导如何做到这一点(数据绑定中的后台更新)?
  • 何时以及如何编组UI线程?
  • 后台线程与之交互的最佳方式是什么 绑定/数据对象?
  • 应该使用哪些类/接口? (BindingSource,...)
  • ...

UI并不真正知道有后台线程,它会更新控件,而且根据我在数据绑定方案中的理解,UI不应该知道数据来自哪里......你可以想到后台线程作为将数据推送到UI的东西,所以我不确定后台工作者是否是我正在搜索的选项。

有时您希望在数据/业务对象的操作期间获得一些UI响应(例如,在重新计算期间设置背景)。在绑定到后台的状态属性上提升属性是不够的,因为控制在计算完成后重新绘制了?我的想法是挂钩propertychanged事件并在控件上调用.update()... 有关于此的任何其他想法吗?

8 个答案:

答案 0 :(得分:6)

这是一个难题,因为大多数“解决方案”导致大量自定义代码以及对BeginInvoke()System.ComponentModel.BackgroundWorker的大量调用(它本身只是BeginInvoke上的一个薄包装)

过去,我还发现您很快就希望延迟发送INotifyPropertyChanged事件,直到数据稳定为止。处理一个适当变更事件的代码通常需要阅读其他属性。你经常有一个控件需要在许多属性之一的状态发生变化时重绘自己,并且你不想让控件过于频繁地重绘。

首先,每个自定义WinForms控件都应该读取它在PropertyChanged事件处理程序中绘制自身所需的所有数据,因此当它是WM_PAINT时,它不需要锁定任何数据对象({{ 1}})消息。控件不应在获取新数据时立即重新绘制;相反,它应该调用OnPaint。 Windows会将Control.Invalidate()消息组合成尽可能少的请求,并且只在UI线程没有其他任何操作时才发送它们。这样可以最大限度地减少重绘次数和数据对象锁定的时间。 (无论如何,标准控件主要通过数据绑定来完成此操作)

数据对象需要记录更改后的更改内容,然后在完成一组更改后,“踢”UI线程调用WM_PAINT方法然后调用{{1所有已更改的属性的事件处理程序(在UI线程上)。在SendChangeEvents方法运行时,必须锁定数据对象以阻止后台线程更新它们。

只要有一组更新从数据库中读取bean,就可以通过调用PropertyChanged来“踢”UI线程。通常最好使用计时器进行UI线程轮询,因为Windows仅在UI消息队列为空时发送SendChangeEvents()消息,从而导致UI感觉更具响应性。

还要考虑根本不使用数据绑定,让UI在每次计时器触发时询问每个数据对象“发生了什么变化”。数据绑定总是很好看,但很快就会成为问题的一部分,而不是解决方案的一部分。

由于数据对象的锁定/解锁很麻烦,并且可能无法足够快地从数据库中读取更新,因此您可能希望将UI线程传递给数据对象的(虚拟)副本。使数据对象具有持久性/不可变性,以便对数据对象的任何更改返回新数据对象而不是更改当前数据对象都可以启用此功能。

持久对象听起来非常慢,但不必如此,请参阅thisthat以获取一些指示。另请查看Stack Overflow上的thisthat

另请查看retlang - Message-based concurrency in .NET。它的消息批处理可能很有用。

(对于WPF,我会在UI线程中设置一个View-Model,然后由后台线程从多线程模型中“批量”更新。但是,WPF在组合数据绑定方面要好得多事件然后WinForms。)

答案 1 :(得分:2)

该主题有一个MSDN article具体内容。但要准备好看看VB.NET。 ;)

另外,也许您可​​以使用System.ComponentModel.BackgroundWorker而不是通用的第二个线程,因为它很好地形式化了与您描述的生成的后台线程的交互类型。 MSDN库中给出的示例相当不错,所以请看一下如何使用它。

编辑: 请注意:如果使用ProgressChanged事件与UI线程进行通信,则不需要编组。后台线程在需要与UI通信时调用ReportProgress。由于可以将任何对象附加到该事件,因此没有理由进行手动编组。进度通过另一个异步操作进行通信 - 因此无需担心UI处理进度事件的速度有多快,也无需担心后台线程是否因等待事件完成而进行干预。

如果您证明后台主题提升了进度更改事件的速度太快,那么您可能需要查看Ayende的优秀文章Pull vs. Push models for UI updates

答案 2 :(得分:2)

是的,所有的书都显示了线程结构和调用等。这是完全正确的等等,但它可能是一个很难编码,并且通常难以组织,所以你可以为它做出体面的测试

UI只需要每秒刷新很多次,因此性能永远不会成为问题,并且轮询可以正常工作

我喜欢使用由后台线程池不断更新的对象图。他们检查数据值的实际变化,当他们注意到实际的变化时,他们会在对象图的根上更新版本计数器(或者在每个主要项目上更新更有意义)并更新值

然后您的前台进程可以有一个计时器(默认情况下与UI线程相同)每秒触发一次并检查版本计数器,如果它发生更改,则锁定它(停止部分更新)然后刷新显示

这种简单的技术完全将UI线程与后台线程隔离开来

答案 3 :(得分:1)

我刚刚遇到了类似的情况 - badkground线程通过BeginInvokes更新UI。每个循环的背景都有10毫秒的延迟,但是我遇到了一些问题,其中UI更新有时会在该循环中每次都被触发,无法跟上更新的频率,并且应用程序有效地停止工作(不确定会发生什么 - 吹了一堆?)。

我最后在通过调用的对象中添加了一个标志,这只是一个准备好的标志。我在调用invoke之前将其设置为false,然后bg线程将不再执行ui更新,直到此标志切换回true。 UI线程将执行它的屏幕更新等,然后将此var设置为true。

这允许bg线程保持运算,但允许ui关闭流程,直到它准备好更多。

答案 4 :(得分:1)

创建一个新的UserControl,添加你的控件并格式化它(可能是dock = fill)并添加一个属性。 现在配置属性以调用usercontrol并更新元素,每次更改所需的任何线程的属性!

这就是我的解决方案:

    private long value;
    public long Value
    {
        get { return this.value; }
        set
        {
            this.value = value;

            UpdateTextBox();
        }
    }

    private delegate void Delegate();
    private void UpdateTextBox()
    {
        if (this.InvokeRequired)
        {
            this.Invoke(new Delegate(UpdateTextBox), new object[] {});
        }
        else
        {
            textBox1.Text = this.value.ToString();
        }
    }
在我的表单上

我绑定了我的视图

viewTx.DataBindings.Add(new Binding("Value", ptx.CounterTX, "ReturnValue"));

答案 5 :(得分:0)

这是我在Update Controls中解决的问题。我提出这一点并不是建议你重写你的代码,而是给你一些资源来寻找想法。

我在WPF中使用的技术是使用Dispatcher.BeginInvoke来通知前台线程更改。您可以在Winforms中使用Control.BeginInvoke执行相同的操作。不幸的是,您必须将对象的引用传递给数据对象。

一旦这样做,您可以将Action传递给触发PropertyChanged的BeginInvoke。例如:

_form.BeginInvoke(new Action(() => NotifyPropertyChanged(propertyName))) );

您需要锁定数据对象中的属性以使其成为线程安全的。

答案 6 :(得分:0)

这篇文章很老但我以为我会给别人选择。一旦你开始进行异步编程和Windows窗体数据绑定,你最终会遇到更新Bindingsource数据源或更新绑定到窗体控件的列表的问题。我将尝试在wintellect上使用他的powerthreading工具中的Jeffrey Richters AsyncEnumerator类。

原因: 1.他的AsyncEnumerator类自动将后台线程封送到UI线程,因此您可以像执行同步代码一样更新控件。 2. AsyncEnumerator简化了异步编程。它会自动执行此操作,因此您以同步方式编写代码,但代码仍以异步方式运行。

杰弗里·里希特(Jeffrey Richter)在第9频道MSDN上播放了一段视频,解释了AsyncEnumerator。

祝你好运。

-R

答案 7 :(得分:0)

我参加晚会很晚,但是我相信这仍然是一个有效的问题。

我建议您完全避免使用数据绑定,而应使用Observable对象。

原因是,数据绑定看起来很酷,实现时代码看起来不错,但是当您进行大量OS异步UI更新或多线程处理时,数据绑定会失败,并失败。

我个人在产品中已经经历了异步和数据绑定的问题,当用户开始使用所有不同的场景时,我们甚至没有在测试中发现它。