绑定源和跨线程异常

本文关键字:线程 异常 绑定 | 更新日期: 2023-09-27 18:05:49

为了解释这个问题,我将所需的所有内容放入一个小的示例应用程序中,希望能解释这个问题。我真的试图把所有东西都推到尽可能少的行中,但在我的实际应用中,这些不同的演员彼此不认识,也不应该。因此,像"将变量放在上面几行并在其上调用 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 中的绑定源的任何信息,则如何解决此跨线程异常。

绑定源和跨线程异常

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

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

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

我知道

你的问题是前段时间提出的,但我决定提交一个答案,以防万一它对那里的人有帮助。

我建议您考虑在主应用程序中订阅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 的每个更新都应该毫无问题地接收和显示。

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

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

In Windows Froms

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

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

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

我知道

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

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

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是本文中其他一些人提到的方便的扩展方法。