INotifyPropertyChanged

Jun 16, 2011 at 2:04 PM

How to get MEF to create ViewModels that implement INotifyPropertyChanged?

The idea is to get MEF to create a DynamicProxy that will intercept the properties setters to fire a PropertyChanged event.

Here is a sample of the corresponding interceptor:

    public class PropertyChangedInterceptor : IInterceptor, INotifyPropertyChanged
    {
        private PropertyChangedEventHandler _handler;
        private bool _first = true;

        public void Intercept(IInvocation invocation)
        {
            if (_first)
            {
                var methodInfo = invocation.Proxy.GetType().GetMethods().Where(
                    m => Attribute.IsDefined(m, typeof (PropertyChangedSinkAttribute))).SingleOrDefault();
                if (methodInfo != null)
                {
                    var sinkDelegate = Delegate.CreateDelegate(typeof (PropertyChangedEventHandler), invocation.Proxy,methodInfo);
                    _handler = (PropertyChangedEventHandler) Delegate.Combine(_handler, sinkDelegate);
                }
                _first = false;
            }

            var methodName = invocation.Method.Name;
            var arguments = invocation.Arguments;
            if (methodName == @"add_PropertyChanged")
            {
                _handler = (PropertyChangedEventHandler)Delegate.Combine(_handler, (Delegate)arguments[0]);
                return;
            }

            if (methodName == @"remove_PropertyChanged")
            {
                _handler = (PropertyChangedEventHandler)Delegate.Remove(_handler, (Delegate)arguments[0]);
                return;
            }
            invocation.Proceed();
            if (invocation.Method.Name.StartsWith(@"set_", StringComparison.Ordinal))
            {
                var propertyName = methodName.Substring(4);
                //indexer case 
                if (2 == arguments.Length)
                {
                    propertyName = propertyName + @"[]";
                }
                var eventHandler = _handler;
                if (eventHandler != null)
                {
                    eventHandler(invocation.Proxy, new PropertyChangedEventArgs(propertyName));
                }
            }
        }

#pragma warning disable 0067
        public event PropertyChangedEventHandler PropertyChanged;
#pragma warning restore 0067

        public void RaisePropertyChanged(object target, LambdaExpression exp)
        {
            var memberExpression = exp.Body as MemberExpression;
            var propertyName = (memberExpression == null) ? null : memberExpression.Member.Name;
            var eventHandler = _handler;
            if (eventHandler != null) eventHandler(target, new PropertyChangedEventArgs(propertyName));
        }
    }


 In order to be able to intercept the properties setters, they need to be virtual and the proxy should be created by ProxyGenerator.CreateClassProxy (in the Castle version) rather than the CreateInterface* variants.Below is a sample of the IExportedValueInterceptor that would create such an proxy:   

public class DynamicProxyInterceptor : IExportedValueInterceptor
    {
        private static readonly ProxyGenerator Generator = new ProxyGenerator();

        private readonly IInterceptor[] _interceptors;

        public DynamicProxyInterceptor(params IInterceptor[] interceptors)
        {
            _interceptors = interceptors;
        }

        public object Intercept(object value)
        {
            Type[] interfacesToProxy = null;
            foreach (var interceptor in _interceptors)
            {
                interfacesToProxy = interceptor.GetType().GetInterfaces().Where(i => i != typeof(IInterceptor)).ToArray();
            }

            return Generator.CreateClassProxy(value.GetType(), interfacesToProxy, _interceptors);
        }
    }


And finally an example of how it would work on the ViewModel:

    [TestFixture]
    public class PropertyChangedInterceptorTest
    {
        private CompositionContainer _container;

        [SetUp]
        public void SetupTestFixture()
        {
            var catalog = new TypeCatalog(typeof(TypicalViewModel), typeof(ViewModelWithImportingConstructor));
            var notifyInterceptor = new PredicateInterceptionCriteria(new DynamicProxyInterceptor(new PropertyChangedInterceptor()),
                                                  d => d.ExportDefinitions.First().Metadata.ContainsKey(AopAnnotation.NotifyPropertyChanged));
            var aopConfig = new InterceptionConfiguration().AddInterceptionCriteria(notifyInterceptor);
            _container = new CompositionContainer(new InterceptingCatalog(catalog, aopConfig));
            //container.ComposeParts(new object[] { this });
        }

        [Test]
        public void CheckProxyRaisesPropertyChanged()
        {
            var vm = _container.GetExportedValue<TypicalViewModel>();

            string propertyName = null;
            ((INotifyPropertyChanged)vm).PropertyChanged += (s, e) => propertyName = e.PropertyName;

            vm.ProxiedProperty = "change now";

            Assert.AreEqual("ProxiedProperty", propertyName);
        }
     }

    [Export]
    [ExportMetadata(AopAnnotation.NotifyPropertyChanged, null)]
    public class TypicalViewModel
    {
        public virtual string ProxiedProperty { get; set; }
    }


So far so good.However most of the time you need to inject some kind of service to your ViewModel. The recommended way of doing this is to use the [ImportingConstructor] as shown below:   

[Export]
    [ExportMetadata(AopAnnotation.NotifyPropertyChanged, null)]
    public class ViewModelWithImportingConstructor
    {

private readonly ISomeService _someService;

        [ImportingConstructor]
        public ViewModelWithImportingConstructor(ISomeService someService)
        {
            _someService = someService;
        }
    }


In this case the proxy generation will fail because there is no parameterless constructor.We should change the proxy generation to pass the constructor arguments.
So in the IExportedValueInterceptor we should call something like this:

return Generator.CreateClassProxy(value.GetType(), constructorParameters, interfacesToProxy, _interceptors);

The constructorParameters are imported parts in MEF (deep in the ImportDefinition of the ComposablePart).
Any idea how to get hold of them, especially when they are imported by MEF?

I look forward to reading your thoughts.

Jun 20, 2011 at 8:25 AM

This had me stuck too. The issue appears to be that InterceptingComposablePart actually creates the object for you - it doesn't give you the option to take over object creation.

To fix this, I think the IExportedValueInterceptor interface needs to be this instead:

object Intercept(Type type, object[] args);

i.e. It passes the type and the constructor arguments (which have already been created by MEF for you) to the interceptor.

The GetExportedValue method in InterceptingComposablePart would then look like this:

/// <summary>
        /// Gets the exported object described by the specified <see cref="ExportDefinition"/> object.
        /// </summary>
        /// <returns>
        /// The exported object described by <paramref name="definition"/>.
        /// </returns>
        /// <param name="definition">One of the <see cref="ExportDefinition"/> objects from the <see cref="ComposablePart.ExportDefinitions"/> property that describes the exported object to return.</param>
        /// <exception cref="ObjectDisposedException">The <see cref="ComposablePart"/> object has been disposed of.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="definition"/> is null.</exception>
        /// <exception cref="ComposablePartException">An error occurred getting the exported object described by the <see cref="ExportDefinition"/>.</exception>
        /// <exception cref="ArgumentException"><paramref name="definition"/> did not originate from the <see cref="ComposablePart.ExportDefinitions"/> property on the <see cref="ComposablePart"/>.</exception>
        /// <exception cref="InvalidOperationException">One or more prerequisite imports, indicated by <see cref="ImportDefinition.IsPrerequisite"/>, have not been set.</exception>
        public override object GetExportedValue(ExportDefinition definition)
        {
            if (definition == null) throw new ArgumentNullException("definition");

            if (this.values.ContainsKey(definition))
            {
                return this.values[definition];
            }

            //Get the export definitions from the import definitions (for the constructor args)
            var exports = InterceptedPart.ImportDefinitions.Where(i => i.IsPrerequisite).SelectMany(i => catalog.GetExports(i));
            //Get instances of the constructor args
            var args = exports.Select(e =>  e.Item1.CreatePart().GetExportedValue(e.Item2)).ToArray();
            //Get the type from the ExportTypeIdentity (perhaps there is a better way to do this?)
            string typename = (string) definition.Metadata["ExportTypeIdentity"];
            string ns = typename.Substring(0, typename.LastIndexOf('.'));
            Type type = Type.GetType(typename + ',' + ns);
            //Intercept the object creation (Now the CreateClassProxy will work correctly)
            var interceptingValue = this.valueInterceptor.Intercept(type, args);

            this.values.Add(definition, interceptingValue);
            return interceptingValue;
        }

Finally, you will notice that I needed a reference to the catalog in order to do this, so, you need to pass this through to InterceptingComposablePart via InterceptingComposablePartDefinition from InterceptingCatalog.

Hopefully, there is a better way to do this because I don't think that this method is particularly clean.

First of all, my method of getting the type seems very wrong. Does anyone know a better way to do this?

Second, I am not sure what to do with other [Import]s in the class. I assume I should do this after the Intercept in GetExportedValue,but I am not sure how to do this. Ideally, I need to be able to call a "SatisfyImports" method on the constructed object, or something like that.

Any thoughts?

Jul 18, 2011 at 11:31 AM

Hmm, it's been a month without reaction from the Mef Contrib team/community.

So we decided to move away from MEF Contrib and proxies to use IL weaving.

The main benefits of this approach are:

  • no interference with MEF (or other container) object instanciations
  • no performance hit

We were reluctant to use Post Sharp, but fortunately there is Afterthought (https://github.com/vc3/Afterthought/wiki).

They even provide an implementation for INotifyPropertyChanged.

 

Coordinator
Jul 18, 2011 at 12:19 PM

I did inform Piotr about this and I know hes been away on vacation.. will poke him again tonight =)

Coordinator
Jul 18, 2011 at 3:17 PM

There's no API in MEF which would allow to get exports which satisfy given constructor. However, is should be pretty straightforward to implement such method, as MEF requirement is for any non-default constructor to be annotated with ImportingConstructorAttribute. So the method might scan constructors of a given type searching for one with that attribute. If found, extract its parameters and retrieve them from the container.

In you case I would use AOP as you described rather than a form of a proxy interception.

Regarding intercepting whole object creation, indeed InterceptingCatalog creates the instance of the exported type even though in an interceptor you would provide your own value (which of course has to obey the contract). I was thinking about extending this mechanism for a long time now. A good approach to this is to divide interception into phases. So instead of a single intercept step, you would have create, intercept and say postinit steps. In create phase you could then create you own exported value, and during the intercept step, if the value has been provided, mef wouldn't even bother creating the instance. Also, i guess it would be nice to provide a context of the interception, from which you could get a reference to the ComposablePartDefinition, which might be useful. Since this is a welcome feature I guess, I will try to have it implemented.

Hope this helps,

Piotr