6

Ich möchte einen dynamischen Proxy für die Bindung von WinForms-Steuerelementen an Objekte erstellen, die von einem anderen Thread (nicht GUI) geändert wurden. Ein solcher Proxy würde das PropertyChanged-Ereignis abfangen und mit dem richtigen SynchronizationContext absetzen.Erstellen eines INotifyPropertyChanged-Proxys zum Verteilen von Aufrufen an den UI-Thread

So könnte ich eine Helper-Klasse verwenden, um die Aufgabe zu erledigen, ohne jedes Mal die Synchronisation manuell implementieren zu müssen (if (control.InvokeRequired) etc.).

Gibt es eine Möglichkeit, das mit LinFu, Castle oder einer ähnlichen Bibliothek zu tun?

[Bearbeiten]

Datenquelle ist nicht unbedingt eine Liste. Es kann jedes Business-Objekt, zum Beispiel sein:

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

Ich konnte einen Wrapper erstellen, die den Job machen könnte, und es würde wie folgt aussehen:

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

(es kann einige Fehler in dieser sein Code, ich mache es)

Was ich tun möchte, ist ein dynamischer Proxy (Reflection.Emit) ein Liner dafür, z

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

und ich wollte wissen, ob so etwas mit vorhandenen dynamischen Proxy-Implementierungen möglich war.

Eine allgemeinere Frage wäre: Wie ein Ereignis beim Erstellen eines dynamischen Proxy abfangen? Abfangen (überschreiben) Eigenschaften wird in allen Implementierungen gut erklärt.

[Edit2]

Der Grund (glaube ich) Ich brauche ein Proxy ist, dass der Stack-Trace wie folgt aussieht:

 
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) 

Sie können sehen, dass BindToObject.PropValueChanged nicht die sender Instanz nicht passiert zu der PropertyManager, und Reflector zeigt, dass Absenderobjekt nirgendwo referenziert wird. Mit anderen Worten, wenn das Ereignis PropertyChanged ausgelöst wird, verwendet die Komponente reflection, um auf die Eigenschaft der ursprünglichen (gebundenen) Datenquelle zuzugreifen.

Wenn ich mein Objekt in eine Klasse verpacken würde, die nur das Ereignis enthält (wie Sam vorgeschlagen), würde eine solche Wrapperklasse keine Eigenschaften enthalten, auf die über Reflection zugegriffen werden könnte.

+0

Siehe 'ThreadedBindingList' - hier auf SO wiederholt wurde (http://stackoverflow.com/questions/455766/how-do-you -correctly-update-a-datensatz-datagridview-from-a-background-thread). –

Antwort

4

Hier ist eine Klasse, die ein INotifyPropertyChanged umschließt, das PropertyChanged-Ereignis über SynchronizationContext.Current weiterleitet und die Eigenschaft weiterleitet.

Diese Lösung sollte funktionieren, aber mit einiger Zeit könnte es verbessert werden, einen Lambda-Ausdruck anstelle eines Eigenschaftsnamens zu verwenden. Das würde erlauben, die Reflexion loszuwerden und einen getippten Zugang zu der Eigenschaft bereitzustellen. Die Komplikation damit ist, dass Sie auch den Ausdrucksbaum aus dem Lambda holen müssen, um den Eigenschaftsnamen herauszuziehen, damit Sie ihn in der OnSourcePropertyChanged-Methode verwenden können. Ich habe einen Beitrag über das Ziehen eines Eigentumsnamens aus einem Lambda-Ausdrucksbaum gesehen, aber ich konnte ihn gerade nicht finden.

diese Klasse verwenden, Sie würden Ihre Bindung wie folgt zu ändern:

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

Und hier ist 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

Danke, das ist im Grunde, was ich am Ende tat, vergaß es zu akzeptieren. Die Sache, die mich störte, war, dass ich immer dachte, dass ich einen einzelnen Wrapper für ein Objekt brauche, während ich eigentlich jede * Eigenschaft * in einen anderen Wrapper umbrechen musste, damit es funktionierte. – Groo

+0

Das ist einfach genial! –

+0

Gerade gefunden, genau das, was ich gesucht habe. Eine Sache jedoch, für jeden anderen, der rüberkommt, ist: der SyncBindingWrapper sollte eine Möglichkeit zur Verfügung stellen, sich vom PropertyChanged-Ereignis des Quellenobjektes zu entfernen, wahrscheinlich, indem IDisposable eingeführt wird. – SimonC

2

ich auf die gleichen Probleme und Samuels Lösung didn gekommen sind‘ Ich arbeite für mich, also habe ich die Synchronisationskontextinitialisierung im Konstruktor platziert, und der "Value" Eigenschaftsname sollte anstelle der ursprünglichen Eigenschaft übergeben werden. Dieser arbeitete für mich:

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

Verbrauch:

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

Ja, danke, IIRC Ich habe auch die Methode auf die gleiche Weise repariert, aber vergessen zu aktualisieren. Die Verwendung von 'SynchronizationContext.Current' in' OnSourcePropertyChanged' würde keinen Sinn ergeben. – Groo