INPC for nested poco bindings

Topics: Conventions, UI Architecture
Mar 9, 2012 at 8:51 AM
Edited Mar 9, 2012 at 8:58 AM

My UI is derived from the HelloScreens sample (simplyfied):

  • ShellViewModel contains items of MasterViewModel<TEntity> : Conductor...
    (Workspaces for all root entities)
    (e.g. CustomerMasterViewModel : MasterViewModel<Customer>, ...)
  • MasterViewModel<TEntity> can open items of DetailViewModel<Customer> : Conductor...
    (e.g. CustomerDetailViewModel : DetailViewModel<Customer>, ...)
  • Each DetailViewModel<TEntity> can have items of TabViewModel<TEntity> : Screen
  • Via MEF and the 'marker' TEntity all parts (Master, Detail, Tabs) are composed automatically. Because all master and detail views are very similar, all implementations are mapped to one MasterView and one DetailView. Only the contents (master: grid, actions, open/recent items etc.; detail: tabs, actions etc.) can vary, so that there are view models and views for all tabs. Some of the tabs show lists that where already loaded with the entity and others just use the fk of the entity to load related data.

DetailViewModel<TEntity>loads an entity and the tab base class TabViewModel<TEntity> can access the entity:

public DetailViewModel<TEntity> ParentDetail { get; set; }
public TEntity Entity { get { return ParentDetail.Entity; } }

A tab view could now bind to the nested POCO property from above:

<TextBox x:Name="Entity_Name"/> 

This would be great, but unfortunatelly INPC is not triggered this way, because my entities don't implement INPC. At the moment the related DetailViewModel<TEntity> only uses the Refresh() method inside the setter of the Entity property and when the active tab is changed.

So the question is:

1) Do I really have to duplicate all view model properties in each tab to implement INPC or is there a better way (kepping CMs nested property bindings)?

I tried notifypropertyweaver, but it would only help with duplicated properties that wrap the nested properties and makes dependent properties and guards a bit more complicated (e.g. CanSave would have to use CustomerName instead of Entity.Name):

public string CustomerName { get {return Entity.Name;} set {Entity.Name = value;}}

A nice solution would be a proxy that adds INPC to a POCO (perhaps in the setter), so that nested bindings like Entity_Name can trigger the change event. Has anyone done something like that yet?

2) Is there a way to apply the desired behavior by customizing or extending CM?

Just off the top of my head: When a binding to a nested property is used (Entity_Name), all changes of the control should trigger INPC of the root property (Entity) by setting Entity=Entity, so that guards and validation related to the root property can be notified by NPCs in the setter of the root property.

Does CM have any extension points to do something like that?

Mar 11, 2012 at 11:49 PM

I have used a solution for this:

My entities are POCO and I have a list view + textbox, when I change the textbox it sets entities Name property and it also refreshes name in list control. The solution I have used for this can be implemented better, more modular but if you want to try a dirty solution look in my project http://moneymanagernet.codeplex.com. This project is not based solely on CM (yet) I wrote it watching Robs video in MIX10.

What I'm doing is setting converter for my POCO. The entity is wrapped in a dynamic class. Look for DynamicNotifyableWrapper. I also keep track of the created instances because when I set a property directly in VM method the code bypasses Wrapper. For this situation I have a helper method to find the wrapper for entity and trigger property changed on it .

protected void RaisePropertyChanged(object contragent, string p)
        {
            var notifyable = identityMap[contragent] as INotifyable;
            if(notifyable!=null)
                notifyable.OnChange(p);
        }

//here is my wrapper

public class DynamicNotifyableWrapper:DynamicObject,INotifyable,INotifyPropertyChanged,IWrapper,ICollectionNotificationRouter
    {

        public void OnChange(string prop)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(prop));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private static readonly Dictionary<string, Dictionary<string, PropertyInfo>> properties =
            new Dictionary<string, Dictionary<string, PropertyInfo>>();
        private readonly object instance;
        private readonly string typeName;

        public DynamicNotifyableWrapper(object instance)
        {
            this.instance = instance;
            var type = instance.GetType();
            typeName = type.FullName;
            if (!properties.ContainsKey(typeName))
                SetProperties(type, typeName);
        }

        private static void SetProperties(Type type, string typeName)
        {
            var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            var dict = props.ToDictionary(prop => prop.Name);
            properties.Add(typeName, dict);
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (properties[typeName].ContainsKey(binder.Name))
            {
                result = properties[typeName][binder.Name].GetValue(instance, null);
                return true;
            }
            result = null;
            return false;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            if (properties[typeName].ContainsKey(binder.Name))
            {
                properties[typeName][binder.Name].SetValue(instance, value, null);
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(binder.Name));
                return true;
            }
            return false;
        }

        #region IWrapper Members

        public object Instance
        {
            get { return this.instance; }
        }

        #endregion



        #region INotifyCollectionChanged Members

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        #endregion

        #region INotifyableCollection Members

        public void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
        {
            if (CollectionChanged != null)
                CollectionChanged(this,args);
        }

        #endregion
    }



 

 

Mar 12, 2012 at 10:54 AM
Edited Mar 12, 2012 at 10:58 AM

Thanks for the hint to DynamicObject. As far as I understand your approach, the DynamicNotifyableWrapper is used inside a IValueConverter and must be specified in all views. I first thought about moving the wrapper to a view model base class, but then the entity would have to be dynamic instread of TEntity or any concrete type (?).

I had a second look at Castle DynamicProxy and a found a solution, which seems to work quite well. The interceptor class is based on a codeplex sample project called TimeKeeper that I downloaded recently (can't remember the author...). It allowes me to use just one entity with nested bindings (like Entity_Name) and doesn't require any property wrappers (CustomerName { get { return Entity.Name; ...) to fire INPC events.

Property inside a view model base class (OnEntityPropertyChanged gets called for property changes inside TEntity):

public TEntity Entity
{
    get { return _entity; }
    set { _entity = ProxyExtension.GetInpcProxy(
            value, _entity, OnEntityPropertyChanged); }
}

Proxy generator (IProxiedEntity just specifies a method GetInstance()):

    public static class ProxyExtension
    {
        private static readonly ProxyGenerator 
            ProxyGenerator = new ProxyGenerator();

        public static T GetInpcProxy<T>(T newValue, T oldValue, PropertyChangedEventHandler eventHandler)
        {
            // create proxy
            var proxy = ProxyGenerator.CreateClassProxyWithTarget(
                typeof(T), new[] {
                    typeof(INotifyPropertyChanged),
                    typeof(IProxiedEntity)
                }, newValue, new EntityInterceptor<T>(newValue));

            // unregister old handler
            var inpcOld = oldValue as INotifyPropertyChanged;
            if (inpcOld != null) inpcOld.PropertyChanged -= eventHandler;

            // register new handler
            var inpcNew = proxy as INotifyPropertyChanged;
            if (inpcNew != null) inpcNew.PropertyChanged += eventHandler;

            return (T)proxy;
        }
    }

Proxy interceptor:

    public class EntityInterceptor<T> : IInterceptor
    {
        private PropertyChangedEventHandler _handler;
        private readonly T _entity;

        public EntityInterceptor(T entity)
        {
            _entity = entity;
        }

        public virtual void Intercept(IInvocation invocation)
        {
            var methodName = invocation.Method.Name;

            // property changed event
            if (invocation.Method.DeclaringType == 
                typeof(INotifyPropertyChanged))
            {
                if (methodName == "add_PropertyChanged")
                    _handler = (PropertyChangedEventHandler)Delegate.Combine(
                        _handler, (Delegate)invocation.Arguments[0]);
                
                if (methodName == "remove_PropertyChanged")
                    _handler = (PropertyChangedEventHandler)Delegate.Remove(
                        _handler, (Delegate)invocation.Arguments[0]);

                return;
            }

            // property set
            Object originalValue = null;
            if (methodName.StartsWith("set_"))
                originalValue = invocation.Proxy.GetType().GetProperty(
                    methodName.Substring(4)).GetValue(invocation.Proxy, null);

            if (methodName == "GetInstance") {
                invocation.ReturnValue = _entity;
                return;
            }

            invocation.Proceed();

            // raise property changed event
            if (methodName.StartsWith("set_") &&
                !Equals(originalValue, invocation.Arguments[0]))
                OnPropertyChanged(invocation.Proxy,
                    new PropertyChangedEventArgs(methodName.Substring(4)));
        }

        protected virtual void OnPropertyChanged(object s, PropertyChangedEventArgs e)
        {
            var handler = _handler;
            if (handler != null)
                handler(s, e);
        }
    }

When loading an entity or assigning a new instance the Entity setter creates the proxy on the fly. Saving a proxied entity via WCF is a bit more difficult, because WCF is expecing a real entity type and refuses to serialize the proxy. Most sample projects I found didn't address this problem. I experimented with DataContractResolver, a custom serializer, cloning and a few other things, but ended up with a simpler solution (also see GetInpcProxy<T> and Intercept(...) ), which has to be used before sending a entity over the wire:

var proxiedEntity = Entity as IProxiedEntity;
var entityToBeSent = (proxiedEntity != null)
        ? (TEntity)proxiedEntity.GetInstance()
        : Entity;