BindingSource和跨线程异常

时间:2011-08-16 14:08:26

标签: c# .net winforms multithreading

为了解释这个问题,我把所需的一切都放到一个小样本应用程序中,希望能够解释这个问题。我真的试图尽可能减少所有内容,但在我的实际应用中,这些不同的演员彼此不认识,也不应该。因此,简单的回答,如“将变量放在上面的几行并调用它上面的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,尝试更新Text的{​​{1}}属性。这显然导致例外。

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

恕我直言,我认为MyData应检查接收对象所在的线程,并做一个合适的BindingSource来获取它们的值。不幸的是,这不是内置的(或者我错了?),所以我的问题是:

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

7 个答案:

答案 0 :(得分:5)

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

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();
};

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

答案 1 :(得分:2)

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

如果你想在你的MyText类和BindingSource之间有一个额外的抽象层,你可以这样做,但你不能将BindngSource与UI线程分开。

答案 2 :(得分:1)

我意识到你的问题是在不久前提出的,但我已经决定提交一个答案,以防它对那里的人有所帮助。

我建议您考虑在主应用程序中订阅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 :(得分:1)

在Windows Froms中

在交叉线程中我刚使用

// this = from 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"
}));

和VOILA

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

如果有可能从同一个线程调用它,那么就添加以下检查

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

答案 4 :(得分:0)

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

答案 5 :(得分:0)

我遇到了类似的情况,我试图从绑定源中删除一条记录,该绑定源绑定到一个UI控件,该控件响应在绑定源上所做的更改。

我在扩展方法中使用了穆罕默德的解决方案。

    /// <summary>
    /// Executes on the UI thread, but calling thread waits for completion before continuing.        
    /// </summary>                
    public static void InvokeIfRequired<T>(this T c, Action<T> action) where T : Control
    {
        if (c.InvokeRequired)            
            c.Invoke(new Action(() => action(c)));            
        else            
            action(c);            
    }

然后可以像这样使用它。

this.InvokeIfRequired(frm => frm.defaultBindingSource.Remove(rec));

这样,无论何时需要调用控件或表单,都只需要一行代码。

我还添加了一个BeginInvokeIfRequired扩展名

    /// <summary>
    /// Executes asynchronously, on a thread pool thread.
    /// </summary>
    public static void BeginInvokeIfRequired<T>(this T c, Action<T> action) where T : Control
    {
        if (c.InvokeRequired)
            c.BeginInvoke(new Action(() => { action(c); }));
        else
            action(c);
    }

如此处所述:difference between Invoke() and BeginInvoke()

答案 6 :(得分:0)

我知道这是一篇过时的文章,但是我只是在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是本文中其他一些人提到的便捷扩展方法。