Creating an INotifyPropertyChanged proxy to dispatch calls to UI thread
Asked Answered
E

3

7

I would like to create a dynamic proxy for binding WinForms controls to objects changed by a different (non-GUI) thread. Such a proxy would intercept the PropertyChanged event and dispatch it using the proper SynchronizationContext.

That way I could use a helper class to do the job, without having to implement the synchronization manually every time (if (control.InvokeRequired) etc.).

Is there a way to do that using LinFu, Castle or a similar library?

[Edit]

Data source is not necessarily a list. It can be any business object, e.g.:

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

I could create a wrapper which could do the job, and it would look something like this:

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

(there may be some errors in this code, I am making it up)

What I would like to do is to have a dynamic proxy (Reflection.Emit) one liner for this, e.g.

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

and I wanted to know if something like this was possible using existing dynamic proxy implementations.

A more general question would be: How to intercept an event when creating a dynamic proxy? Intercepting (overriding) properties is explained well in all implementations.

[Edit2]

The reason (I think) I need a proxy is that the stack trace looks like this:

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)

You can see that BindToObject.PropValueChanged does not pass the sender instance to the PropertyManager, and Reflector shows that sender object is not referenced anywhere. In other words, when the PropertyChanged event is triggered, component will use reflection to access the property of the original (bound) data source.

If I wrapped my object in a class containing only the event (as Sam proposed), such wrapper class would not contain any properties which could be accessed through Reflection.

Erbe answered 25/1, 2010 at 20:5 Comment(1)
See ThreadedBindingList - it has been repeated here on SO (#456266).Do
H
5

Here's a class that will wrap a INotifyPropertyChanged, forward the PropertyChanged event through SynchronizationContext.Current, and forward the property.

This solution should work, but with some time it could be improved to use a lambda expression instead of a property name. That would allow getting rid the reflection, provide typed access to the property. The complication with this is you need to also get the expression tree from the lambda to pull out the property name so you can use it in the OnSourcePropertyChanged method. I saw a post about pulling a property name from a lambda expression tree but I couldn't find it just now.

To use this class, you'd want to change your binding like this:

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

And here's 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);
    }
}
Hugues answered 27/1, 2010 at 15:16 Comment(3)
Thanks, this is basically what I did at the end, forgot to accept it. The thing that bothered me was that I kept thinking that I need a single wrapper for an object, while actually I needed to wrap each property in a different wrapper to make it work.Erbe
Just found this, exactly what I was looking for. One thing though, for anyone else that comes across is: the SyncBindingWrapper should provide a means to remove itself from the PropertyChanged event of the source object, probably by implementing IDisposable.Stuff
This code is not working. SynchronizationContext.Current is null when called from the other thread. It should be initialized in the constructor.Perrault
P
3

I have come across the same problems and Samuel's solution didn't work for me, so I placed the synchronization context initialization in the constructor, and the "Value" property name should be passed instead of the original property. This worked for me:

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

Usage:

_textBox1.DataBindings.Add("Text", new SyncBindingWrapper(someObject, "SomeProperty"), "Value");
Perrault answered 18/2, 2013 at 11:18 Comment(1)
Yes, thanks, IIRC I also fixed the method in the same manner but forgot to update. Using SynchronizationContext.Current in OnSourcePropertyChanged wouldn't make sense.Erbe
D
0

Without relying on the SynchrnoisationConext you can rely on ISynchronizeInvoke

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);
        }
    }
}
Disgraceful answered 5/12, 2019 at 13:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.