【问题标题】:BindingSource and Cross-Thread exceptionsBindingSource 和跨线程异常
【发布时间】:2011-08-16 14:08:26
【问题描述】:

为了解释这个问题,我将所需的一切都放入一个小示例应用程序中,希望能解释这个问题。我真的试图把所有的东西都推得尽可能少,但在我的实际应用中,这些不同的演员彼此不认识,也不应该认识。因此,像“将变量放在上面几行并对其调用 Invoke”这样简单的答案是行不通的。

让我们从代码开始,然后再做一些解释。一开始有一个实现 INotifyPropertyChanged 的​​简单类:

public class MyData : INotifyPropertyChanged
{
    private string _MyText;

    public MyData()
    {
        _MyText = "Initial";
    }

    public string MyText
    {
        get { return _MyText; }

        set
        {
            _MyText = value;
            PropertyChanged(this, new PropertyChangedEventArgs("MyText"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

所以没什么特别的。这里的示例代码可以简单地放入任何空的控制台应用程序项目中:

static void Main(string[] args)
{
    // Initialize the data and bindingSource
    var myData = new MyData();
    var bindingSource = new BindingSource();
    bindingSource.DataSource = myData;

    // Initialize the form and the controls of it ...
    var form = new Form();

    // ... the TextBox including data bind to it
    var textBox = new TextBox();
    textBox.DataBindings.Add("Text", bindingSource, "MyText");
    textBox.DataBindings.DefaultDataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged;
    textBox.Dock = DockStyle.Top;
    form.Controls.Add(textBox);

    // ... the button and what happens on a click
    var button = new Button();
    button.Text = "Click me";
    button.Dock = DockStyle.Top;
    form.Controls.Add(button);

    button.Click += (_, __) =>
    {
        // Create another thread that does something with the data object
        var worker = new BackgroundWorker();

        worker.RunWorkerCompleted += (___, ____) => button.Enabled = true;
        worker.DoWork += (___, _____) =>
        {
            for (int i = 0; i < 10; i++)
            {
                // This leads to a cross-thread exception
                // but all i'm doing is simply act on a property in
                // my data and i can't see here that any gui is involved.
                myData.MyText = "Try " + i;
            }
        };

        button.Enabled = false;
        worker.RunWorkerAsync();
    };

    form.ShowDialog();
}

如果您将运行此代码,您将通过尝试更改 MyText 属性来获得跨线程异常。这会导致MyData 对象调用PropertyChanged,这将被BindindSource 捕获。然后,根据Binding,这将尝试更新TextBoxText 属性。这显然导致了异常。

我最大的问题来自MyData 对象不应该知道关于 gui 的任何信息(因为它是一个简单 数据对象)。此外,工作线程对 gui 也一无所知。它只是作用于一堆数据对象并对其进行操作。

恕我直言,我认为BindingSource 应该检查接收对象在哪个线程上,并执行适当的Invoke() 以获得它们的值。不幸的是,这不是内置的(或者我错了吗?),所以我的问题是:

如果数据对象和工作线程知道任何关于正在监听其事件以将数据推送到 gui 的绑定源的信息,如何解决此跨线程异常。

【问题讨论】:

    标签: c# .net winforms multithreading


    【解决方案1】:

    这是解决此问题的上述示例的一部分:

    button.Click += (_, __) =>
    {
        // Create another thread that does something with the data object
        var worker = new BackgroundWorker();
    
        worker.DoWork += (___, _____) =>
        {
            for (int i = 0; i < 10; i++)
            {
                // This doesn't lead to any cross-thread exception
                // anymore, cause the binding source was told to
                // be quiet. When we're finished and back in the
                // gui thread tell her to fire again its events.
                myData.MyText = "Try " + i;
            }
        };
    
        worker.RunWorkerCompleted += (___, ____) =>
        {
            // Back in gui thread let the binding source
            // update the gui elements.
            bindingSource.ResumeBinding();
            button.Enabled = true;
        };
    
        // Stop the binding source from propagating
        // any events to the gui thread.
        bindingSource.SuspendBinding();
        button.Enabled = false;
        worker.RunWorkerAsync();
    };
    

    所以这不会再导致任何跨线程异常。此解决方案的缺点是您不会在文本框中显示任何中间结果,但总比没有好。

    【讨论】:

      【解决方案2】:

      我知道您的问题是不久前提出的,但我决定提交一个答案,以防万一它对那里的人有帮助。

      我建议您考虑在主应用程序中订阅 myData 的属性更改事件,然后更新您的 UI。下面是它的样子:

      //This delegate will help us access the UI thread
      delegate void dUpdateTextBox(string text);
      
      //You'll need class-scope references to your variables
      private MyData myData;
      private TextBox textBox;
      
      static void Main(string[] args)
      {
          // Initialize the data and bindingSource
          myData = new MyData();
          myData.PropertyChanged += MyData_PropertyChanged;
      
          // Initialize the form and the controls of it ...
          var form = new Form();
      
          // ... the TextBox including data bind to it
          textBox = new TextBox();
          textBox.Dock = DockStyle.Top;
          form.Controls.Add(textBox);
      
          // ... the button and what happens on a click
          var button = new Button();
          button.Text = "Click me";
          button.Dock = DockStyle.Top;
          form.Controls.Add(button);
      
          button.Click += (_, __) =>
          {
              // Create another thread that does something with the data object
              var worker = new BackgroundWorker();
      
              worker.RunWorkerCompleted += (___, ____) => button.Enabled = true;
              worker.DoWork += (___, _____) =>
              {
                  for (int i = 0; i < 10; i++)
                  {
                      myData.MyText = "Try " + i;
                  }
              };
      
              button.Enabled = false;
              worker.RunWorkerAsync();
          };
      
          form.ShowDialog();
      }
      
      //This handler will be called every time "MyText" is changed
      private void MyData_PropertyChanged(Object sender, PropertyChangedEventArgs e)
      {
          if((MyData)sender == myData && e.PropertyName == "MyText")
          {
              //If we are certain that this method was called from "MyText",
              //then update the UI
              UpdateTextBox(((MyData)sender).MyText);
          }
      }
      
      private void UpdateTextBox(string text)
      {
          //Check to see if this method call is coming in from the UI thread or not
          if(textBox.RequiresInvoke)
          {
              //If we're not on the UI thread, invoke this method from the UI thread
              textBox.BeginInvoke(new dUpdateTextBox(UpdateTextBox), text);
              return;
          }
      
          //If we've reached this line of code, we are on the UI thread
          textBox.Text = text;
      }
      

      当然,这消除了您之前尝试的绑定模式。但是,MyText 的每次更新都应该被接收并显示,没有问题。

      【讨论】:

        【解决方案3】:

        如果绑定到 winforms 控件,则无法从另一个线程更新 BindingSource。在您的 MyText 设置器中,您必须在 UI 线程上 Invoke PropertyChanged 而不是直接运行它。

        如果您想在 MyText 类和 BindingSource 之间增加一层抽象,您可以这样做,但您不能将 BindngSource 与 UI 线程分开。

        【讨论】:

        • 但问题是我不知道MyData 类中的UI 线程。那么如果我无权访问表单上当前的任何控件来调用Invoke(),那么如何调用 UI 线程呢?
        • @Oliver:嘿,你解决了这个问题?我也被这样的事情困住了?
        • @mahesh:为这个问题添加一个自己的答案,关于我是如何解决这个问题的。它并不完美,但总比没有好。
        • @Oliver:我是这个 Windows 窗体应用程序的新手。我添加了一个关于的问题。你能看看..stackoverflow.com/questions/14258488/…
        【解决方案4】:

        在 Windows 中

        在我刚刚使用的交叉线程中

        // this = form on which listbox control is created.
        this.Invoke(new Action(() => 
        {
           //you can call all controls it will not raise exception of cross thread 
           //example 
           SomeBindingSource.ResetBindings(false); 
           Label1.Text = "any thing"
           TextBox1.Text = "any thing"
        }));
        

        然后瞧

        ////////// 编辑 //////////

        如果有机会从创建它的同一线程调用,则添加以下检查

        // this = form on which listbox control is created.  
             if(this.InvokeRequired)
                 this.Invoke(new Action(() => { SomeBindingSource.ResetBindings(false); }));
             else
                 SomeBindingSource.ResetBindings(false);
        

        【讨论】:

        • 完美解决方案!
        【解决方案5】:

        您可以尝试从后台线程报告进度,这将在 UI 线程中引发事件。或者,您可以在调用 DoWork 之前尝试记住当前上下文(您的 UI 线程),然后在 DoWork 中,您可以使用记住的上下文来发布数据。

        【讨论】:

        • 不幸的是,在我的真实应用程序中,它不是从正在更新数据的 UI 线程启动的 BackgroundWorker。事实上,它是一个拥有对象列表并简单地处理它们的线程。这些对象也在绑定到数据网格的第二个列表中。那么对象本身如何知道它们是一些必须作用于 ui 线程的事件侦听器呢?接收者是否有责任确保他们在正确的上下文中处理事件数据?
        • @Oliver 我认为你是对的,这取决于接收者应该知道在哪里处理数据。你可以尝试通过事件来做到这一点吗?您可以定义 DataReceived 事件并从 UI 订阅它(类似于报告进度)。列表中的对象不知道任何侦听器,它们只是从对象的线程中触发事件,CLR 将自动管理接收者的上下文。如果您使用事件,则无需监视哪个线程触发和接收。没有?
        • 对象只是在它自己的线程中触发它的事件,正确的。然后绑定源(在对象线程内)接收它并自动尝试更新目标(也在对象线程内),这会导致跨线程异常。 BindingSource 属于 ui 线程,但它不检查它是否从 ui 线程触发,我认为这是 Microsoft 的设计缺陷,不是吗?
        【解决方案6】:

        我知道这是一篇旧帖子,但我刚刚在一个 winforms 应用程序上遇到了这个问题,这似乎有效。

        我创建了一个 BindingSource 的子类并截获了 OnListChanged 处理程序以在 UI 线程上调用。

        public class MyBindingSource : BindingSource
            {
                private readonly ISynchronizeInvoke context;
        
                protected override void OnListChanged(ListChangedEventArgs e)
                {
                    if (context == null) base.OnListChanged(e);
                    else context.InvokeIfRequired(c => base.OnListChanged(e));
                }
        
                public MyBindingSource(ISynchronizeInvoke context = null)
                {
                    this.context = context;
                }
            }
        

        InvokeIfRequired 是本文其他人提到的便捷扩展方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-11-08
          • 2010-10-23
          • 1970-01-01
          相关资源
          最近更新 更多