8
\$\begingroup\$

As a part of my (future) MVVM framework, I need to be able to detect changes on an object. To do this, I used the RealProxy, a C# object that implements Aspect Oriented Programming to "intercept" calls to an object's method.

In my scenario, I'll use the proxy to intercept calls to a _target's setter to launch an event, PropertyChanged.

public delegate void PropertyChanged(PropertyChangedArgs args);

/// <summary>
/// Proxy that intercepts call to methods
/// </summary>
/// <typeparam name="T">Type of the target</typeparam>
internal class ObservableObjectProxy<T> : RealProxy where T : MarshalByRefObject
{
    private const string SetterMethodStart = "set_";
    private readonly T _target;

    public event PropertyChanged PropertyChanged;

    /// <summary>
    /// Creates a new instance of ObservableObjectProxy
    /// </summary>
    /// <param name="target">Object to proxy</param>
    public ObservableObjectProxy(T target) : base(typeof(T))
    {
        if (target == null) throw new ArgumentNullException(nameof(target));

        _target = target;
    }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        if (methodCall == null)
            throw new NotSupportedException("Invocation must be done on a method");

        var result = InvokeMethod(methodCall);

        if (!IsSetterCall(methodCall))
            return result;

        var interceptedArgs = new PropertyChangedArgs(methodCall.MethodName.Substring(4),methodCall.GetArg(0));

        PropertyChanged?.Invoke(interceptedArgs);

        return result;
    }

    private IMethodReturnMessage InvokeMethod(IMethodCallMessage callMsg)
    {
        return RemotingServices.ExecuteMessage(_target, callMsg);
    }

    private static bool IsSetterCall(IMethodCallMessage method)
    {
        return method.MethodName.StartsWith(SetterMethodStart);
    }
}

This object, as you've seen, is internal. To create it, I'm using a factory.

public interface IObservableObjectFactory
{
    T Wrap<T>(T obj) where T : MarshalByRefObject;
    T Create<T>() where T : MarshalByRefObject, new();
    T Create<T>(params object[] parameters) where T : MarshalByRefObject;
}

/// <summary>
/// Creates or wrap an object that will be observed by the controller.
/// </summary>
public class ObservableObjectFactory : IObservableObjectFactory
{
    private readonly IObserverController _observerController;

    /// <summary>
    /// Creates an instance of ObservableObjectFactory
    /// </summary>
    /// <param name="observerController">Controller that receives the <code>ObservableObjectProxy</code>'s PropertyChanged event. Cannot be null.</param>
    public ObservableObjectFactory(IObserverController observerController)
    {
        if (observerController == null) throw new ArgumentNullException(nameof(observerController));
        _observerController = observerController;
    }

    /// <summary>
    /// Creates a new instance of type <typeparamref name="T"/> that is observed by the controller
    /// </summary>
    /// <typeparam name="T">Type of the object to create</typeparam>
    /// <returns></returns>
    public T Create<T>() where T : MarshalByRefObject, new()
    {
        ObservableObjectProxy<T> proxy = new ObservableObjectProxy<T>(new T());
        proxy.PropertyChanged += _observerController.Handle;
        return (T)proxy.GetTransparentProxy();
    }

    /// <summary>
    /// Creates a new instance of type <typeparamref name="T"/> that is observed by the controller
    /// </summary>
    /// <typeparam name="T">Type of the object to create</typeparam>
    /// <param name="parameters">Parameters to supply to object's constructor</param>
    /// <returns></returns>
    public T Create<T>(params object[] parameters) where T : MarshalByRefObject
    {
        var obj = (T)Activator.CreateInstance(typeof(T), parameters);

        ObservableObjectProxy<T> proxy = new ObservableObjectProxy<T>(obj);
        proxy.PropertyChanged += _observerController.Handle;
        return (T)proxy.GetTransparentProxy();
    }

    /// <summary>
    /// Wraps an existing object in an <code>ObservableObjectProxy</code> that is observed by the controller.
    /// </summary>
    /// <typeparam name="T">Type of the object to wrap</typeparam>
    /// <param name="obj">Object to wrap</param>
    /// <returns></returns>
    public T Wrap<T>(T obj) where T : MarshalByRefObject
    {
        ObservableObjectProxy<T> proxy = new ObservableObjectProxy<T>(obj);
        proxy.PropertyChanged += _observerController.Handle;
        return (T)proxy.GetTransparentProxy();
    }
}

This factory is used to create or wrap an object and attach the Handle method of the controller to the created object. Here's the controller's interface :

public interface IObserverController
{
    void Handle(PropertyChangedArgs args);
}

And the args for the event :

public class PropertyChangedArgs
{
    public string PropertyName { get; set; }
    public object Value { get; set; }

    public PropertyChangedArgs(string propertyName, object value)
    {
        if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
        if (value == null) throw new ArgumentNullException(nameof(value));

        PropertyName = propertyName;
        Value = value;
    }
}

I'm looking to know if my code :

  • Makes a good use of the event pattern.
  • Makes a good use of the RealProxy.

Obviously, anything else is welcomed!

\$\endgroup\$
6
\$\begingroup\$

Attention Bug Alert

The way how the IsSetterCall() method is implemented is flawed. The method doesn't clearly distinguish between a property and a method. I could call a method like

public void set_Test() { }  

and your method would assume this is a property which wouldn't cause any problems here, but later in the Invoke() method you use methodCall.GetArg(0) which would cause a TargetParameterCountException being thrown.

To be at least a little bit more on the safe side of life you can change the IsSetterCall() method like so

private static bool IsSetterCall(IMethodCallMessage method)
{
    return method.MethodBase.IsSpecialName && method.MethodName.StartsWith(SetterMethodStart);
}

but to have a real safe way you would need to add a little bit reflection voodoo like so

private static bool IsSetterCall(IMethodCallMessage method)
{
    var methodBase = method.MethodBase;
    if (methodBase.IsSpecialName && method.MethodName.StartsWith(SetterMethodStart))
    {
        var property = methodBase.DeclaringType.GetProperty(method.MethodName.Substring(SetterMethodStart.Length),
                    BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);

        return property != null 
            && property.GetSetMethod() == methodBase as MethodInfo;
    }

    return false;
}

public class PropertyChangedArgs

The name of this class does not follow the event naming guidelines which states

Name an event argument class with the EventArgs suffix.

which means you should change the name to PropertyChangedEventArgs.

The property setters of this class should by private because you don't want that this properties can be set from outside of the class.

Why don't you allow null for the Value? This will restrict you from setting a property to null which is legit.

Why do you allow an empty or whitespaced string to be passed for the PropertyName? This maybe won't happen in your current szenario but who knows what the future brings.

Another "problem" I see with the whole of your event stuff is that it doesn't follow the typical event pattern which contains a sender.


IObserverController

It isn't clear from this interface what it should be used for. The only hint will be that it has a Handle() method which has a PropertyChangedArgs as a parameter. One could assume that it should be used to handle the PropertyChanged event but this isn't clear from the way the interface is named/written.

Why not simply call the interface IPropertyChangedHandler ?


Style

   if (!IsSetterCall(methodCall))
        return result;  

Please don't ever omit barces {} although they might be optional. Such style can lead to bugs which are hard to discover.

Although this style

if (target == null) throw new ArgumentNullException(nameof(target));  

is slightly better than having the braceless instruction on the next line of code it wouldn't hurt to use braces here as well.

\$\endgroup\$
8
\$\begingroup\$

Are you reinventing the wheel?

I'm sorry if this addresses just a little part of your question, but if you're looking for a way to get an event when a property is changed, have you thought about implementing INotifyPropertyChanged (msdn)?

With this you could do stuff like:

public class Person : INotifyPropertyChanged 
{
    ...
    public string PersonName 
    {
        get { return name; }
        set { name = value; OnPropertyChanged("PersonName"); }
    }
}

And with some accompanying stuff you'll get event handling almost for free. I've also seen a similar pattern, but can't find it just now, where you set the new value as part of the property change handling.

In other words, you might have reinvented the wheel, and on the flip side I might have misunderstood your question.

A few style comments

Here goes for a more proper review of your code, not focusing on the reinvention which you seems to want to do.

  • Magic number within Invoke > var interceptedArgs = ... - You do a call to Substring(4) which presumably refers to the text after SetterMethodStart, but this should then be the length of this variable in case it got changed or someone decides to use a more common setVariableName structure in their code.
  • Is SetterMethodStart named according to conventions? – According to this you are correctly naming it like you've done, but as this is extremely similar to the MethodNames I'm inclined to suggesting to use SETTER_METHOD_START or _SetherMethodStart. But then again that is kind of against the guidelines...

How do you use it?!

The last point I would like to make, is that I still don't quite get how you want to practically use this. And neither do I quite understand how you separate from get notified on all changes to any property or what/where your filter code is.

As such, there might be something said regarding useful comments within your code describing use cases and the point of using your class. The code and comments you have now are somewhat obvious and at the same time unclear as to the real purpose of your code.

A typical example is "Proxy that intercepts call to methods" which kind of says what it does, but then again why does it do that, and what is the purpose of doing this? Maybe a better comment would be "Intercepts calls to setter methods (a.k.a. methods prefixed with set_), and for these invokes PropertyChanged.Invoke with arguments`"

\$\endgroup\$
  • \$\begingroup\$ No you kind of understood why I did it this way. I'm aware of the INotifyPropertyChanged, but I decided to try with the proxy. I should probably benchmark both to make sure the performance hit of RealProxy isn't too strong. The proxy is interesting because it needs no code in the class to "listen" to. I should still take a look at your option to see the differences. \$\endgroup\$ – IEatBagels Dec 9 '15 at 21:12
  • \$\begingroup\$ @TopinFrassi What I really like with this variant (and/or the one I can't find just now), is the simplicity of using it and the small code print. How do you use your variant by the way? \$\endgroup\$ – holroy Dec 9 '15 at 21:14
  • \$\begingroup\$ @TopinFrassi most of the time this pattern will suffice but it's insufficient for when you need more than the name of the property you changed, like when you want the values of parameters passed to the method, etc. It especially becomes necessary to use something like RealProxy when dependency inversion requires you to not be able to reference the method scope from your aspect. \$\endgroup\$ – moarboilerplate Dec 9 '15 at 21:15
  • \$\begingroup\$ @TopinFrassi might I recommend using an interceptor aspect already baked in to your IoC of choice, say for example, Castle Windsor's IInterceptors? \$\endgroup\$ – moarboilerplate Dec 9 '15 at 21:16
  • \$\begingroup\$ @holroy My variant automagically calls Invoke method when a method is called (getter/setters included) so I can launch my PropertyChanged event by myself! So well, I don't need to write the OnPropertyChanged everywhere. \$\endgroup\$ – IEatBagels Dec 9 '15 at 21:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.