2011-08-16 17 views
6

Para explicar este problema, pongo todo lo que necesita en una pequeña aplicación de muestra que con suerte explica el problema. Realmente traté de introducir todo lo menos posible, pero en mi aplicación real, estos diferentes actores no se conocen entre sí y tampoco deberían hacerlo. Entonces, una respuesta simple como "tomar la variable unas líneas arriba y llamar a Invoke on it" no funcionaría.Excepciones BindingSource y Cross-Thread

Comencemos con el código y luego un poco más de explicación. Al principio hay una clase simple que implementa 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; 
} 

Así que nada de especial. Y aquí el código de ejemplo, que simplemente se pueden poner en cualquier proyecto de aplicación de consola vacía:

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

Si desea ejecutar este código se podrían obtener una excepción de hilo cruzado al tratar de cambiar la propiedad MyText. Esto viene, porque el objeto MyData llama al PropertyChanged, que quedará atrapado por el BindindSource. Esto entonces, de acuerdo con Binding, intenta actualizar la propiedad Text del TextBox. Lo que claramente conduce a la excepción.

Mi mayor problema aquí viene del hecho de que el objeto MyData no sabe nada acerca de una interfaz gráfica de usuario (la causa es un simple objeto de datos). Además, el hilo de trabajo no sabe nada sobre una GUI. Simplemente actúa sobre un conjunto de objetos de datos y los manipula.

En mi humilde opinión, creo que el BindingSource debe comprobar en qué hilo está viviendo el objeto receptor y hacer un apropiado Invoke() para obtener el valor de ellos. Desafortunadamente esto no está incorporado (¿o estoy equivocado?), Entonces mi pregunta es:

Cómo se puede resolver esta excepción entre hilos si el objeto de datos o el hilo de trabajo no saben nada sobre una fuente de enlace que está escuchando para sus eventos para insertar los datos en una interfaz gráfica de usuario.

Respuesta

4

Aquí es la parte del ejemplo anterior que resuelve este problema:

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

Así que esto no conduce a ninguna excepción hilos cruz más. El inconveniente de esta solución es que no se mostrarán resultados intermedios en el cuadro de texto, pero es mejor que nada.

2

No puede actualizar BindingSource desde otro hilo si está vinculado a un control de formas de ganar. En su setter de MyText debe Invoke PropertyChanged en el hilo de UI en lugar de ejecutarlo directamente.

Si desea una capa adicional de abstracción entre su clase MyText y BindingSource, puede hacerlo, pero no puede separar BindngSource del subproceso UI.

+1

Pero el problema es que no sé el hilo de interfaz de usuario dentro de la clase 'MyData'. Entonces, ¿cómo invocar el hilo de UI si no tengo acceso a ningún control que esté actualmente en el formulario para invocar 'Invoke()' en él? – Oliver

+0

@Oliver: oye, ¿has resuelto este problema? también estoy atrapado con algo como esto? –

+0

@mahesh: Agregue una respuesta propia a esta pregunta, sobre cómo he resuelto esto. No es perfecto, pero es mejor que nada. – Oliver

0

Puede intentar informar el progreso del subproceso en segundo plano que provocará un evento en el subproceso de interfaz de usuario. Alternativamente, puede intentar recordar el contexto actual (su cadena de interfaz de usuario) antes de llamar al DoWork y luego dentro del DoWork puede usar el contexto recordado para publicar datos.

+0

Desafortunadamente en mi aplicación real no es un BackgroundWorker iniciado a partir del subproceso de UI que está actualizando los datos. De hecho, es un hilo que tiene una lista de objetos y simplemente funciona en ellos. Estos objetos también están en una segunda lista, que está vinculada a una cuadrícula de datos. Entonces, ¿cómo sabe el propio objeto que son algunos oyentes de eventos que tienen que actuar sobre el hilo de la interfaz de usuario? ¿No es responsabilidad de los receptores asegurarse de que procesen los datos del evento en el contexto correcto? – Oliver

+0

@Oliver Creo que tienes razón y los receptores deben saber dónde procesar los datos. ¿Puedes intentar hacer esto a través de los eventos? Puede definir un evento DataReceived y suscribirse a él desde la UI (similar al progreso del informe). Los objetos de la lista no tienen conocimiento de ningún oyente, solo activan el evento a partir del hilo de los objetos, y CLR administrará los contextos del receptor automáticamente. Si usa eventos, no hay necesidad de controlar qué hilo se dispara y cuál recibe. ¿No? – oleksii

+0

El objeto simplemente activa su evento en su propio hilo, correcto. A continuación, la fuente de enlace lo recibe (dentro de la cadena de objetos) y automáticamente intenta actualizar el objetivo (también dentro de la cadena de objetos) lo que lleva a una excepción de cadena cruzada. BindingSource pertenece al hilo de la interfaz de usuario, pero no comprueba si se dispara desde el hilo de la interfaz de usuario y creo que esto es un defecto de diseño de Microsoft, ¿no? – Oliver

1

Me doy cuenta de que su pregunta fue planteada hace un tiempo, pero he decidido enviar una respuesta en caso de que sea útil para alguien.

Le sugiero que considere suscribirse al evento de propiedad cambiado de myData dentro de su aplicación principal y luego actualice su UI. Esto es lo que podría parecer:

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

De acuerdo, esto elimina el patrón de unión que estaba intentando antes. Sin embargo, cada actualización de MyText debería recibirse y mostrarse sin problemas.

0

En Windows froms

En hilo cruzado que acabo de utilizar

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

y listo