2009-12-10 13 views
6

Tengo unINotifyPropertyChanged con hilos

BindingList<T> 

que está unido a un DataGridView. Una propiedad en mi clase requiere mucho tiempo para calcular, así que enhebré la acción. Después del cálculo elevo el evento OnPropertyChanged() para notificar a la grilla que el valor está listo.

Al menos, esa es la teoría. Pero como el método OnPropertyChanged se llama desde un hilo diferente, obtengo algunas excepciones weired en el método OnRowPrePaint de la grilla.

¿Alguien puede decirme cómo preveo que el evento OnPropertyChanged se ejecute en el hilo principal? No puedo usar Form.Invoke, ya que la clase MyClass no sabe que se ejecuta en una aplicación Winforms.

public class MyClass : INotifyPropertyChanged 
{ 
    public int FastMember {get;set;} 

    private int? slowMember; 
    public SlowMember 
    { 
     get 
     { 
      if (slowMember.HasValue) 
       return slowMember.Value; 
      else 
      { 
       Thread t = new Thread(getSlowMember); 
       t.Start(); 
       return -1; 
      } 

     } 
    } 

    private void getSlowMember() 
    { 
     Thread.Sleep(1000); 
     slowMember = 5; 
     OnPropertyChanged("SlowMember"); 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 
    private void OnPropertyChanged(string propertyName) 
    { 
     PropertyChangingEventHandler eh = PropertyChanging; 
     if (eh != null) 
     { 
      eh(this, e); 
     } 
    } 

} 

Respuesta

8

Por diseño, un control solo se puede actualizar por el hilo en el que se creó. Es por eso que está recibiendo excepciones.

Considere usar un BackgroundWorker y solo actualice el miembro después de que se haya completado la operación de larga duración mediante la suscripción de un manejador de eventos al RunWorkerCompleted.

+0

Funciona como un encanto. Hasta ahora no sabía acerca de BackgroundWorker. Eso hace que esta tarea sea tan fácil, mucho. –

1

Consideración 1:
Tome un vistazo a la clase UIThreadMarshal y su uso en este artículo:
UI Thread Marshaling in the Model Layer
Puede cambiar la clase de estático a instancia y lo inyecta en el objeto. Entonces su objeto no sabrá sobre la clase de Forma. Solo sabrá sobre la clase UIThreadMarshal.

Consideración 2:
No creo que devolver -1 desde su propiedad es una buena idea. Me parece un mal diseño.

Consideración 3:
Tal vez su clase no debería usar otro hilo. Tal vez sean las clases de consumidores las que decidan cómo llamar a su propiedad: directamente o en un hilo separado. En este caso, tal vez necesite proporcionar propiedades adicionales, como IsSlowMemberInitialized.

+0

A 1: gracias por el enlace. The BackgroundWorker resolvió mi problema en este caso, pero apuesto a que mis pantalones cortos lo necesitaré en el futuro cercano. A 2: Tiene razón, especialmente porque SlowMember puede ser -1. Fue solo para probar Para 3: No es posible, porque DataGridView consulta el valor (y obtiene -1 por primera vez, que actualizo el valor y uso la interfaz INotifyPropertyChanged para informar la vista de cuadrícula de datos de la propiedad modificada, que tiene que sucederá en el hilo principal. (De acuerdo, podría usar un temporizador y comprobar si IsSlowMemberInitialized = verdadero, pero eso es feo. De todos modos, mucho. –

+0

Si usa DataGridView, entonces tal vez necesite usar BindingSource. En el enlace que le di, hay una implementación de un BindingSource que admite la vinculación de diferentes hilos. Puede trabajar en ese código para que sea más adecuado para sus necesidades. – nightcoder

2

Aquí hay algo que escribí hace un tiempo; que debería funcionar razonablemente bien, pero tenga en cuenta el costo de un montón de actualizaciones ...

using System.ComponentModel; 
using System.Threading; 
public class ThreadedBindingList<T> : BindingList<T> { 
    SynchronizationContext ctx = SynchronizationContext.Current; 
    protected override void OnAddingNew(AddingNewEventArgs e) { 
     if (ctx == null) { BaseAddingNew(e); } 
     else { ctx.Send(delegate { BaseAddingNew(e); }, null); } 
    } 
    protected override void OnListChanged(ListChangedEventArgs e) { 
     if (ctx == null) { BaseListChanged(e); } 
     else { ctx.Send(delegate { BaseListChanged(e); }, null); } 
    } 
    void BaseListChanged(ListChangedEventArgs e) { base.OnListChanged(e); } 
    void BaseAddingNew(AddingNewEventArgs e) { base.OnAddingNew(e); } 
} 
+0

Implementación interesante Marc, pero como esto permite un mal diseño, creo que solo debería usarse en ciertos escenarios donde realmente necesita el control para actualizar mientras se procesa la acción. –

6

La gente a veces se olvidan de que el controlador de eventos es un MultiCastDelegate y, como tal, tiene toda la información relativa a cada suscriptor que Debe manejar esta situación con elegancia sin imponer innecesariamente la penalización de rendimiento Invoke + Synchronization. He estado usando código como este para las edades:

using System.ComponentModel; 
// ... 

public event PropertyChangedEventHandler PropertyChanged; 

protected virtual void OnPropertyChanged(string propertyName) 
{ 
    var handler = PropertyChanged; 
    if (handler != null) 
    { 
     var e = new PropertyChangedEventArgs(propertyName); 
     foreach (EventHandler h in handler.GetInvocationList()) 
     { 
      var synch = h.Target as ISynchronizeInvoke; 
      if (synch != null && synch.InvokeRequired) 
       synch.Invoke(h, new object[] { this, e }); 
      else 
       h(this, e); 
     } 
    } 
} 

lo que hace es simple, pero recuerdo que casi me fracturé mi cerebro en ese entonces tratando de encontrar la mejor manera de hacerlo.

Primero "agarra" el controlador de eventos en una propiedad local para evitar cualquier condición de carrera.

Si el controlador no es nulo (al menos un suscriptor existe) prepara los argumentos del evento y luego recorre la lista de invocación de este delegado de multidifusión.

La lista de invocación tiene la propiedad de destino, que es el suscriptor del evento.Si este suscriptor implementa ISynchronizeInvoke (todos los controles de UI lo implementan), entonces revisamos su propiedad InvokeRequired, y es cierto que solo lo invocamos pasando el delegado y los parámetros. Llamarlo de esta manera sincronizará la llamada en el hilo de UI.

De lo contrario, simplemente llamamos directamente al delegado del controlador de eventos.

+2

Tuve que renombrar 'EventHandler' a' PropertyChangedEventHandler' porque estaba obteniendo 'System.InvalidCastException' con el detalle' {"No se pudo lanzar el objeto del tipo 'System.ComponentModel.PropertyChangedEventHandler' para escribir 'System.EventHandler'." } ' Tengo un BindingList que se crea en el subproceso de la interfaz de usuario que se suscribe al evento internamente, pero la variable de sincronización siempre devuelve nulo porque h.Target es nulo. –

+0

Tengo el mismo problema que @RickShealer. Al anotar la fecha, me pregunto si esto es un problema con las versiones más recientes de .Net. Esto parece una solución muy elegante para el problema INhotifyPropertyChanged de cruce de hilos, así que espero que podamos hacerlo funcionar. – Jacob

+0

@Jacob apuntaré a los marcos más nuevos para ver si fallan. ¿Me puede decir cuál es la versión objetivo de su proyecto o cualquier otra información que pueda considerar pertinente? – Loudenvier

Cuestiones relacionadas