Spring AOP + MVVM Foundation + PropertyChanged

2019-08-12 06:23发布

问题:

I'm using Spring.Net 1.3.1 alongside MVVM Foundation to apply cross-cutting to my viewmodels. I've noticed that if I assign a property changed handler before the object is converted to a proxy for cross-cutting that the proxy engine does not apply the property changed handler to the proxy. Does anyone know if this is expected behavior and if so, if there is a workaround?

My factory looks like this

public static class AopProxyFactory {
    public static object GetProxy(object target) {
        var factory = new ProxyFactory(target);

        factory.AddAdvisor(new Spring.Aop.Support.DefaultPointcutAdvisor(
                                new AttributeMatchMethodPointcut(typeof(AttributeStoringMethod)),
                                new UnitValidationBeforeAdvice())
                           );

        factory.AddAdvice(new NotifyPropertyChangedAdvice());
        factory.ProxyTargetType = true;

        return factory.GetProxy();
    }
}

The advices look like this

    public class UnitValidationBeforeAdvice : IMethodBeforeAdvice {
    public UnitValidationBeforeAdvice() {            
    }

    public void Before(MethodInfo method, object[] args, object target) {
        if (args.Length != 1) {
            throw new ArgumentException("The args collection is not valid!");
        }

        var canConvertTo = true;
        if (!canConvertTo) {
            throw new ArgumentException("The '{0}' cannot be converted.");
        }
    }
}

public class NotifyPropertyChangedAdvice : IAfterReturningAdvice, INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged;

    public void AfterReturning(object ReturnValue, MethodInfo Method, object[] Args, object Target) {
        if (Method.Name.StartsWith("set_")) {
            RaisePropertyChanged(Target, Method.Name.Substring("set_".Length));
        }
    }

    private void RaisePropertyChanged(Object Target, String PropertyName) {
        if (PropertyChanged != null)
            PropertyChanged(Target, new PropertyChangedEventArgs(PropertyName));
    }
}

The object I'm proxying look like this

    public class ProxyTypeObject : ObservableObject {
    private string whoCaresItsBroke;
    public string WhoCaresItsBroke {
        get { return whoCaresItsBroke; }
        set {
            whoCaresItsBroke = value;
            RaisePropertyChanged("WhoCaresItsBroke");
        }
    }
}

And the calling code

var pto = new ProxyTypeObject();
                pto.WhoCaresItsBroke = "BooHoo";
                pto.PropertyChanged += (object sender, System.ComponentModel.PropertyChangedEventArgs e) => {
                    return;
                };

                var proxy = AopProxyFactory.GetProxy(pto);
                (proxy as ProxyTypeObject).WhoCaresItsBroke = "BooHoo2";

You will notice that when I set the "WhoCaresItsBroke" property the property changed handler I previously hooked up is never hit. (I tried using the NotifyPropertyChangedAdvice as provided in the spring.net forums but that does not appear to work.)

回答1:

It seems that the Spring examples Spring.AopQuickStart\src\Spring.AopQuickStart.Step6 does almost the samething you are trying to do (intercepting the [autogenerated] setter of a Property). You might want to have a look at the source of the example.



回答2:

You should declare the WhoCaresItsBroke property as virtual, otherwise it will not be overridden by your proxy object. Making it virtual will cause your handler on pto to be called again, because the proxy will delegate the property call to its target.

You do not need the NotifyPropertyChangedAdvice, you can remove it. The desired behavior is already implemented by the ObservableObject class you're using.

If you want the PropertyChanged event to be fired on the proxy when the target PropertyChanged event is fired, you should implement this manually, as suggested in the following hack.

The hack or workaround to fire PropertyChanged on proxy and the target

A proxyfactory does not wire target events to similar event on a proxy, but you could do this manually. I'm not sure if I would advice you to do so, but you could use the following hack.

Rewrite your proxy factory and ProxyTypeObject:

public class ProxyTypeObject : ObservableObject
{
    private string whoCaresItsBroke;
    // step 1:
    // make the property virtual, otherwise it will not be overridden by the proxy
    public virtual string WhoCaresItsBroke
    {
      // original implementation
    }

    public void PublicRaisePropertyChanged(string name)
    {
        RaisePropertyChanged(name);
    }
}

public static class AopProxyFactory
{
    public static object GetProxy(object target)
    {
        ProxyFactory factory = GetFactory(target);

        object proxy = factory.GetProxy();

        if(target is ProxyTypeObject)
        {
            // step 2:
            // hack: hook handlers ...
            var targetAsProxyTypeObject = (ProxyTypeObject)target;
            var proxyAsProxyTypeObject = (ProxyTypeObject)proxy;
            HookHandlers(targetAsProxyTypeObject, proxyAsProxyTypeObject);
        }

        return proxy;

    }

    private static void HookHandlers(ProxyTypeObject target, ProxyTypeObject proxy)
    {
        target.PropertyChanged += (sender, e) =>
        {
            proxy.PublicRaisePropertyChanged(e.PropertyName);
        };
    }

    private static ProxyFactory GetFactory(object target)
    {
        var factory = new ProxyFactory(target);
        // I simply add the advice here, but you could useyour original
        //  factory.AddAdvisor( ... )
        factory.AddAdvice(new UnitValidationBeforeAdvice());
        // You don't need this:
        // factory.AddAdvice(new NotifyPropertyChangedAdvice()); 
        factory.ProxyTargetType = true;
        return factory;
    }
}

This requires ProxyTypeObject to have a publicly visible method to raise a PropertyChangedEvent; you probably should do this differently, but that's besides the point.

How it works

The factory returns a proxy of type ProxyTypeObject, because you have set factory.ProxyTargetType = true;. It is still a composition based proxy though: after proxying you will have the original object (the target) and the new proxy object. Both proxy and target are of type ProxyTypeObject and can raise a PropertyChanged event.

At this stage, when setting WhoCaresItsBroke on the proxy, the PropertyChanged event will fire on your proxy, but not on the target. The target property is not changed.

step 1: declare property as virtual

Because we've made the property WhoCaresItsBroke virtual, it can be overridden in the proxy. In the overridden property, the proxy object delegates all WhoCaresItsBroke calls to the WhoCaresItsBroke property to the target.

After this step, you'll see that the original handler you attached to your pto instance is called. However, the PropertyChanged event on the proxy is not raised.

step 2: hook target event to a handler on proxy

Simply hook the target PropertyChanged event to a handler on the proxy that raises its own PropertyChanged event. We can use the same name because in the proxy we can assume we're of the same type.