2011-08-16 117 views
6

爲了解釋這個問題,我把所有需要的東西放到一個小樣本應用程序中,希望能夠解釋這個問題。我真的試圖儘可能減少所有行數,但在我的實際應用中,這些不同的演員彼此不認識,也不應該這樣做。所以,簡單的答案就像「將上面的幾行變量並調用Invoke」不起作用。BindingSource和跨線程異常

所以讓我們從代碼開始,然後再解釋一點。起初有一個簡單的類實現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中。

回答

4

下面是解決這個問題上面的例子中的一部分:

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

如果綁定到winforms控件,則無法從另一個線程更新BindingSource。在您的MyText設置器中,您必須在UI線程上使用Invoke PropertyChanged,而不是直接運行它。

如果你想在你的MyText類和BindingSource之間有一個額外的抽象層,你可以這樣做,但是你不能將BindngSource和UI線程分開。

+1

但問題是,我不知道'MyData'類中的UI線程。那麼如何調用UI線程,如果我沒有訪問任何當前在窗體上調用'Invoke()'的控件? – Oliver

+0

@Oliver:嘿,你解決了這個問題?我也堅持這樣的事情? –

+0

@mahesh:爲這個問題添加一個自己的答案,我如何解決這個問題。這並不完美,但總比沒有好。 – Oliver

0

您可以嘗試從後臺線程報告進度,這將引發UI線程中的事件。或者,您可以在調用DoWork之前嘗試記住當前上下文(您的UI線程),然後在DoWork之內使用記住的上下文來發布數據。

+0

不幸的是我真正的應用程序,它不是從正在更新數據的UI線程開始的BackgroundWorker。事實上,它是一個具有對象列表的線程,並且只是對它們起作用。這些對象也在第二個列表中,綁定到數據網格。那麼對象本身如何知道它們是一些事件監聽器必須在UI線程上執行?接收者有責任確保他們在正確的環境中處理事件數據嗎? – Oliver

+0

@Oliver我認爲你是對的,這取決於接收者應該知道在哪裏處理數據。你可以嘗試通過事件做到這一點嗎?您可以定義DataReceived事件並從UI訂閱(與報告進度類似)。列表中的對象不知道任何偵聽器,它們只是從對象的線程中觸發事件,並且CLR將自動管理接收者的上下文。如果使用事件,則不需要監視哪個線程觸發和哪個線程接收。沒有? – oleksii

+0

對象只是在它自己的線程中觸發它的事件,是正確的。然後綁定源接收它(在對象線程內)並自動嘗試更新導致跨線程異常的目標(也在對象線程內)。 BindingSource屬於ui線程,但它不檢查它是否從ui線程觸發,我認爲這是Microsoft的設計缺陷,不是嗎? – Oliver

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都應該被接收並且沒有問題地顯示。

0

在Windows櫨

在橫線我只是用

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