2011-08-16 35 views
6

Để giải thích vấn đề này tôi đặt tất cả mọi thứ cần thiết vào một ứng dụng mẫu nhỏ mà hy vọng giải thích vấn đề. Tôi thực sự cố gắng đẩy mọi thứ vào càng ít dòng càng tốt, nhưng trong ứng dụng thực sự của tôi, những diễn viên khác nhau không biết nhau và cũng không nên. Vì vậy, câu trả lời đơn giản như "lấy biến một vài dòng ở trên và gọi Invoke trên nó" sẽ không hoạt động.BindingSource và Cross-Thread ngoại lệ

Vì vậy, hãy bắt đầu với mã và sau đó giải thích thêm một chút. Ban đầu, có một lớp đơn giản triển khai 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; 
} 

Vì vậy, không có gì đặc biệt. Và đây mã ví dụ mà chỉ đơn giản có thể được đưa vào bất kỳ dự án giao diện điều khiển ứng dụng rỗng:

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

Nếu bạn muốn chạy mã này, bạn sẽ nhận được một ngoại lệ chéo chủ đề bằng cách cố gắng để thay đổi MyText tài sản. Điều này xảy ra, gây ra các đối tượng MyData gọi PropertyChanged sẽ bị bắt bởi số BindindSource. Sau đó, theo số Binding, hãy cố gắng cập nhật thuộc tính Text của số TextBox. Mà rõ ràng dẫn đến ngoại lệ.

Vấn đề lớn nhất của tôi ở đây xuất phát từ thực tế là đối tượng MyData không nên biết bất kỳ điều gì về gui (do đó là đơn giản đối tượng dữ liệu). Ngoài ra các chủ đề công nhân không biết bất cứ điều gì về một gui. Nó chỉ đơn giản là hành động trên một loạt các đối tượng dữ liệu và thao túng chúng.

IMHO tôi nghĩ rằng BindingSource nên kiểm tra xem chuỗi nào đối tượng nhận đang sống và thực hiện một cách thích hợp Invoke() để nhận giá trị của chúng. Tuy nhiên, câu hỏi của tôi là:

Làm thế nào có thể giải quyết ngoại lệ cross-thread này nếu đối tượng dữ liệu và chuỗi công nhân không biết gì về nguồn ràng buộc đang lắng nghe cho các sự kiện của họ để đẩy dữ liệu vào gui.

Trả lời

4

Dưới đây là một phần của các ví dụ trên có thể giải quyết vấn đề này:

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

Vì vậy, điều này không dẫn đến bất kỳ trường hợp ngoại lệ chéo sợi nữa. Hạn chế của giải pháp này là bạn sẽ không nhận được bất kỳ kết quả trung gian nào được hiển thị bên trong hộp văn bản, nhưng nó tốt hơn là không có gì.

2

Bạn không thể cập nhật BindingSource từ chuỗi khác nếu nó bị ràng buộc với một điều khiển winforms. Trong setter MyText của bạn, bạn phải Invoke PropertyChanged trên thread UI thay vì chạy nó trực tiếp.

Nếu bạn muốn thêm lớp trừu tượng giữa lớp MyText và BindingSource bạn có thể làm điều đó, nhưng bạn không thể tách BindngSource khỏi chuỗi giao diện người dùng.

+1

Nhưng vấn đề là tôi không biết thread UI trong lớp 'MyData'. Vì vậy, làm thế nào để gọi thread UI nếu tôi không có quyền truy cập vào bất kỳ điều khiển hiện đang ở trên biểu mẫu để gọi 'Invoke()' trên nó? – Oliver

+0

@Oliver: hey bạn đã giải quyết vấn đề này? tôi cũng bị mắc kẹt với một cái gì đó như thế này? –

+0

@mahesh: Thêm câu trả lời cho câu hỏi này, về cách tôi giải quyết vấn đề này. Nó không hoàn hảo, nhưng tốt hơn là không có gì. – Oliver

0

Bạn có thể thử báo cáo tiến trình từ chuỗi nền sẽ làm tăng sự kiện trong chuỗi giao diện người dùng. Ngoài ra, bạn có thể thử nhớ ngữ cảnh hiện tại (chuỗi giao diện người dùng của bạn) trước khi gọi DoWork và sau đó bên trong DoWork bạn có thể sử dụng ngữ cảnh được ghi nhớ để đăng dữ liệu.

+0

Thật không may trong ứng dụng thực sự của tôi, nó không phải là một BackgroundWorker bắt đầu từ thread UI đang cập nhật dữ liệu. Trong thực tế nó là một chủ đề có một danh sách các đối tượng và chỉ đơn giản là làm việc trên chúng. Các đối tượng này cũng nằm trong một danh sách thứ hai, được gắn với một DataGrid. Vì vậy, làm thế nào để các đối tượng chính nó biết rằng họ là một số người nghe sự kiện phải hành động trên sợi ui? Nó không phải là trách nhiệm của người nhận để đảm bảo rằng họ xử lý dữ liệu sự kiện trong bối cảnh đúng? – Oliver

+0

@Oliver Tôi nghĩ bạn đúng và tùy thuộc vào người nhận nên biết nơi xử lý dữ liệu. Bạn có thể thử làm điều này thông qua các sự kiện không? Bạn có thể xác định một sự kiện DataReceived và đăng ký nó từ giao diện người dùng (Tương tự như tiến trình báo cáo). Các đối tượng trong danh sách không nhận thức được bất kỳ người nghe nào, chúng chỉ kích hoạt sự kiện từ chủ đề của đối tượng và CLR sẽ tự động quản lý bối cảnh của người nhận. Nếu bạn sử dụng các sự kiện, không cần phải giám sát chủ đề nào phát ra và nhận được chủ đề nào. Không? – oleksii

+0

Đối tượng chỉ đơn giản là kích hoạt sự kiện của nó trong chủ đề riêng của nó, chính xác. Sau đó, các nguồn ràng buộc nhận được nó (trong thread đối tượng) và tự động cố gắng để cập nhật các mục tiêu (cũng trong thread đối tượng) dẫn đến một ngoại lệ cross-thread. Các BindingSource thuộc về thread ui, nhưng nó không kiểm tra xem nó cháy từ thread ui và tôi nghĩ rằng đây là một lỗ hổng thiết kế của Microsoft, phải không? – Oliver

1

Tôi nhận thấy rằng câu hỏi của bạn đã được đặt ra cách đây một thời gian, nhưng tôi đã quyết định gửi câu trả lời chỉ trong trường hợp nó hữu ích cho người nào đó ở ngoài đó.

Tôi khuyên bạn nên cân nhắc việc đăng ký sự kiện đã thay đổi thuộc tính của myData trong ứng dụng chính của bạn, sau đó cập nhật giao diện người dùng của bạn. Dưới đây là những gì nó có thể trông giống như:

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

Được cấp, điều này không có mẫu ràng buộc bạn đã thử trước đây. Tuy nhiên, mọi bản cập nhật cho MyText đều phải được nhận và hiển thị mà không có vấn đề gì.

0

Trong Windows froms

Trong chủ đề cross tôi chỉ sử dụng

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

và thì đấy

Các vấn đề liên quan