6

Me gustaría crear un proxy dinámico para vincular controles de WinForms a objetos modificados por un hilo diferente (no GUI). Tal proxy interceptaría el evento PropertyChanged y lo enviaría utilizando el SynchronizationContext adecuado.Crear un proxy INotifyPropertyChanged para enviar llamadas al hilo de la interfaz de usuario

De esta forma podría usar una clase auxiliar para hacer el trabajo, sin tener que implementar la sincronización manualmente cada vez (if (control.InvokeRequired) etc.).

¿Hay alguna manera de hacerlo usando LinFu, Castle o una biblioteca similar?

[Editar]

origen de datos no es necesariamente una lista. Puede ser cualquier objeto de negocio, por ejemplo:

interface IConnection : INotifyPropertyChanged 
{ 
    ConnectionStatus Status { get; } 
} 

que podría crear una envoltura que podría hacer el trabajo, y sería algo como esto:

public class ConnectionWrapper : IConnection 
{ 
    private readonly SynchronizationContext _ctx; 
    private readonly IConnection _actual; 
    public ConnectionWrapper(IConnection actual) 
    { 
     _ctx = SynchronizationContext.Current; 
     _actual= actual; 
     _actual.PropertyChanged += 
      new PropertyChangedEventHandler(actual_PropertyChanged); 
    } 

    // we have to do 2 things: 
    // 1. wrap each property manually 
    // 2. handle the source event and fire it on the GUI thread 

    private void PropertyChanged(object sender, PropertyChangedEvArgs e) 
    { 
     // we will send the same event args to the GUI thread 
     _ctx.Send(delegate { this.PropertyChanged(sender, e); }, null); 
    } 

    public ConnectionStatus Status 
    { get { return _instance.Status; } } 

    public event PropertyChangedEventHandler PropertyChanged; 
} 

(puede haber algunos errores en este código, lo estoy inventando)

Lo que me gustaría hacer es tener un proxy dinámico (Reflection.Emit) un liner para esto, por ejemplo

IConnection syncConnection 
     = new SyncPropertyChangedProxy<IConnection>(actualConnection); 

y quería saber si algo como esto era posible utilizando las implementaciones de proxy dinámico existentes.

Una pregunta más general sería: ¿Cómo se intercepta un evento al crear un proxy dinámico? Las propiedades de intercepción (anulación) se explican bien en todas las implementaciones.

[Edit2]

La razón (creo) que necesito un proxy es que el seguimiento de la pila se ve así:

 
at PropertyManager.OnCurrentChanged(System.EventArgs e) 
at BindToObject.PropValueChanged(object sender, EventArgs e) 
at PropertyDescriptor.OnValueChanged(object component, EventArgs e) 
at ReflectPropertyDescriptor.OnValueChanged(object component, EventArgs e) 
at ReflectPropertyDescriptor.OnINotifyPropertyChanged(object component, 
    PropertyChangedEventArgs e)  
at MyObject.OnPropertyChanged(string propertyName) 

Se puede ver que BindToObject.PropValueChanged no pasa la instancia sender al PropertyManager, y Reflector muestra que el objeto remitente no se referencia en ninguna parte. En otras palabras, cuando se desencadena el evento PropertyChanged, el componente utilizará la reflexión para acceder a la propiedad de la fuente de datos original (encuadernada).

Si envolví mi objeto en una clase que solo contenía el evento (como Sam propuesto), dicha clase contenedora no contendría ninguna propiedad a la que se pudiera acceder a través de Reflection.

+0

Véase 'ThreadedBindingList' - se ha repetido aquí en la SO (http://stackoverflow.com/questions/455766/how-do-you -correctly-update-a-databound-datagridview-from-a-background-thread). –

Respuesta

4

Aquí hay una clase que ajustará un INotifyPropertyChanged, reenviará el evento PropertyChanged a través de SynchronizationContext.Current y reenviará la propiedad.

Esta solución debería funcionar, pero con el tiempo podría mejorarse para usar una expresión lambda en lugar de un nombre de propiedad. Eso permitiría deshacerse del reflejo, proporcionar acceso mecanografiado a la propiedad. La complicación con esto es que también necesita obtener el árbol de expresiones de la lambda para extraer el nombre de la propiedad para que pueda usarlo en el método OnSourcePropertyChanged. Vi una publicación sobre cómo sacar un nombre de propiedad de un árbol de expresiones lambda, pero no pude encontrarlo en este momento.

Para utilizar esta clase, que le quiere cambiar su unión como esta:

Bindings.Add("TargetProperty", new SyncBindingWrapper<PropertyType>(source, "SourceProperty"), "Value"); 

y aquí está SyncBindingWrapper:

using System.ComponentModel; 
using System.Reflection; 
using System.Threading; 

public class SyncBindingWrapper<T> : INotifyPropertyChanged 
{ 
    private readonly INotifyPropertyChanged _source; 
    private readonly PropertyInfo _property; 

    public event PropertyChangedEventHandler PropertyChanged; 

    public T Value 
    { 
     get 
     { 
      return (T)_property.GetValue(_source, null); 
     } 
    } 

    public SyncBindingWrapper(INotifyPropertyChanged source, string propertyName) 
    { 
     _source = source; 
     _property = source.GetType().GetProperty(propertyName); 
     source.PropertyChanged += OnSourcePropertyChanged; 
    } 

    private void OnSourcePropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     if (e.PropertyName != _property.Name) 
     { 
      return; 
     } 
     PropertyChangedEventHandler propertyChanged = PropertyChanged; 
     if (propertyChanged == null) 
     { 
      return; 
     } 

     SynchronizationContext.Current.Send(state => propertyChanged(this, e), null); 
    } 
} 
+1

Gracias, esto es básicamente lo que hice al final, se olvidó de aceptarlo. Lo que me molestaba era que seguía pensando que necesitaba una sola envoltura para un objeto, mientras que en realidad necesitaba envolver cada * propiedad * en un envoltorio diferente para que funcionara. – Groo

+0

¡Esto es simplemente genial! –

+0

Acabo de encontrar esto, exactamente lo que estaba buscando. Una cosa, sin embargo, para cualquier otra persona que se encuentre es: SyncBindingWrapper debería proporcionar un medio para eliminarse del evento PropertyChanged del objeto fuente, probablemente implementando IDisposable. – SimonC

2

me he encontrado con los mismos problemas y solución de Samuel dejase' t trabajo para mí, así que coloqué la inicialización del contexto de sincronización en el constructor, y se debe pasar el nombre de la propiedad "Value" en lugar de la propiedad original. Esto funcionó para mí:

public class SyncBindingWrapper: INotifyPropertyChanged 
{ 
    private readonly INotifyPropertyChanged _source; 
    private readonly PropertyInfo _property; 

    public event PropertyChangedEventHandler PropertyChanged; 

    private readonly SynchronizationContext _context; 

    public object Value 
    { 
     get 
     { 
      return _property.GetValue(_source, null); 
     } 
    } 

    public SyncBindingWrapper(INotifyPropertyChanged source, string propertyName) 
    { 
     _context = SynchronizationContext.Current; 
     _source = source; 
     _property = source.GetType().GetProperty(propertyName); 
     source.PropertyChanged += OnSourcePropertyChanged; 
    } 

    private void OnSourcePropertyChanged(object sender, PropertyChangedEventArgs e) 
    { 
     var propertyChanged = PropertyChanged; 
     if (propertyChanged != null && e.PropertyName == _property.Name) 
     { 
      _context.Send(state => propertyChanged(this, new PropertyChangedEventArgs("Value")), null); 
     } 
    } 
} 

Uso:

_textBox1.DataBindings.Add("Text", new SyncBindingWrapper(someObject, "SomeProperty"), "Value"); 
+0

Sí, gracias, IIRC También arreglé el método de la misma manera pero olvidé actualizar. Usar 'SynchronizationContext.Current' en' OnSourcePropertyChanged' no tendría sentido. – Groo

Cuestiones relacionadas