【问题标题】:WinForms multi-threaded databinding scenario, best practice?WinForms多线程数据绑定场景,最佳实践?
【发布时间】:2010-10-10 19:19:16
【问题描述】:

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

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

所以我想到了几个问题:

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

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

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

【问题讨论】:

    标签: winforms multithreading data-binding


    【解决方案1】:

    我迟到了,但我相信这仍然是一个有效的问题。

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

    原因是,数据绑定看起来很酷,并且在实现时代码看起来不错,但是当你的情况有很多 os 异步 UI 更新或多线程时,数据绑定会惨遭失败。

    我在 prod 中亲身经历过异步和数据绑定的问题,我们甚至在测试中都没有发现它,当用户开始使用所有不同的场景时,事情就开始崩溃了。

    【讨论】:

      【解决方案2】:

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

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

      Jeffrey Richter 在 Channel 9 MSDN 上有一个视频,解释了 AsyncEnumerator。

      祝我好运。

      -R

      【讨论】:

        【解决方案3】:

        有一个专门针对该主题的MSDN article。但是准备好看看 VB.NET。 ;)

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

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

        如果您证明后台线程将进度更改事件提升得太快,那么您可能需要查看 Pull vs. Push models for UI updates Ayende 的一篇优秀文章。

        【讨论】:

        • System.ComponentModel.BackgroundWorker,可能是解决方案的一部分,但难题是如何在不查看 UI 线程的情况下保持数据快速更新。执行上述操作而无需考虑锁定或跨线程调用的所有其他代码行。
        • 如果您查看 Backgroundworker,那么使用 WindowsFormsSynchronizationContext 生成线程和编组并没有太大区别,BW 也是如此。
        【解决方案4】:

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

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

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

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

        只要一组更新从数据库中读取了 bean,就可以通过调用 BeginInvoke 来“踢” UI 线程。通常最好使用计时器让 UI 线程轮询,因为 Windows 仅在 UI 消息队列为空时发送 WM_TIMER 消息,从而使 UI 感觉更灵敏。

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

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

        持久对象听起来很慢,但不一定是,请参阅thisthat 以获得一些指示。另请查看 Stack Overflow 上的 thisthat

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

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

        【讨论】:

          【解决方案5】:

          创建一个新的用户控件,添加您的控件并对其进行格式化(也许停靠=填充)并添加一个属性。 现在配置属性以调用用户控件并更新您的元素,每次您从任何线程更改属性时!

          这就是我的解决方案:

              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"));
          

          【讨论】:

            【解决方案6】:

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

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

            这允许 bg 线程继续运行,但允许 ui 关闭流,直到它准备好更多。

            【讨论】:

            • 唯一不确定的部分是为什么允许从两个线程访问标志。但它似乎有效。
            【解决方案7】:

            是的,所有书籍都显示了线程结构和调用等。这是完全正确的等,但编写代码可能会很痛苦,而且通常很难组织,因此您可以对其进行体面的测试

            一个 UI 只需要每秒刷新这么多次,所以性能从来不是问题,轮询可以正常工作

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

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

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

            【讨论】:

              【解决方案8】:

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

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

              完成后,您可以将一个 Action 传递给 BeginInvoke 以触发 PropertyChanged。例如:

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

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2011-03-09
                • 1970-01-01
                • 2014-06-04
                • 1970-01-01
                • 1970-01-01
                • 2018-11-03
                • 1970-01-01
                相关资源
                最近更新 更多