Исключения BindingSource и Cross-Thread - PullRequest
7 голосов
/ 16 августа 2011

Чтобы объяснить эту проблему, я поместил все необходимое в небольшое приложение, которое, как мы надеемся, объяснит проблему. Я действительно старался сделать все как можно меньше, но в моем реальном приложении эти разные актеры не знают друг друга и тоже не должны. Таким образом, простой ответ типа «возьмите переменную несколькими строками выше и вызовите для нее 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 TextBox. Что явно приводит к исключению.

Моя самая большая проблема здесь заключается в том, что объект MyData не должен ничего знать о графическом интерфейсе (потому что это простой объект данных). Также рабочий поток ничего не знает о графическом интерфейсе. Он просто воздействует на кучу объектов данных и манипулирует ими.

ИМХО, я думаю, BindingSource должен проверить, в каком потоке живет принимающий объект, и сделать соответствующее Invoke(), чтобы получить значение их. К сожалению, это не встроено в него (или я ошибаюсь?), Поэтому мой вопрос:

Как разрешить это межпотоковое исключение, если объект данных или рабочий поток ничего не знают об источнике привязки, который прослушивает их события, чтобы отправить данные в графический интерфейс.

Ответы [ 5 ]

5 голосов
/ 11 января 2013

Вот часть приведенного выше примера, которая решает эту проблему:

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 голосов
/ 16 августа 2011

Вы не можете обновить BindingSource из другого потока, если он связан с элементом управления winforms. В вашем установщике MyText вы должны Invoke PropertyChanged в потоке пользовательского интерфейса, а не запускать его напрямую.

Если вам нужен дополнительный уровень абстракции между вашим классом MyText и BindingSource, вы можете это сделать, но вы не можете отделить BindngSource от потока пользовательского интерфейса.

1 голос
/ 15 февраля 2018

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

и ВОЙЛА

/////////// Редактировать //////////

Если существует вероятность вызова из того же потока, в котором он создан, добавьте следующую проверку

// this = from on which listbox control is created.  
     if(this.InvokeRequired)
         this.Invoke(new Action(() => { SomeBindingSource.ResetBindings(false); }));
     else
         SomeBindingSource.ResetBindings(false);
1 голос
/ 08 июля 2014

Я понимаю, что ваш вопрос был задан некоторое время назад, но я решил представить ответ на тот случай, если он будет полезен кому-то там.

Я предлагаю вам рассмотреть возможность подписки на событие, измененное свойством myData, вваше основное приложение, затем обновите ваш интерфейс.Вот как это может выглядеть:

//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 должно приниматься и отображаться без проблем.

0 голосов
/ 16 августа 2011

Вы можете попробовать сообщить о прогрессе из фонового потока, который вызовет событие в потоке пользовательского интерфейса.В качестве альтернативы вы можете попробовать запомнить текущий контекст (ваш поток пользовательского интерфейса) перед вызовом DoWork, а затем внутри DoWork вы можете использовать запомненный контекст для публикации данных.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...