Freigeben über


(WF4) ModelItem, ModelItemImpl and ICustomTypeDescriptor

[Update 08/03/11: fixed a few horrible proofreading issues which slipped past my nonexistant editor]

Today we’re going to explore some internal workings of ModelItem - or really its concrete subclass ModelItemImpl. ModelItemImpl is an internal class, making all of what we talk about below ‘undocumented implementation detail’, (according to some narrow view based on documenting only APIs which are marked ‘public’), and therefore potentially invalid in future framework versions. Now stop worrying about such minor details and read on, because this stuff is interesting!

Setting the context. What is ModelItem for, anyway?

When you getting started with writing custom activity designers, and even if you have done WPF before, you'll notice it's kind of mysterious that this word 'ModelItem' shows up pervasively all the activity designer examples.

<TextBox Content="{Binding ModelItem.OwnerName}”/>

What’s the point of all this?

Using ModelItem gives you an important feature for making WPF binding work, which is change notification from a wrapped object. Instead of binding straight to a literal property on your activity, instead you bind to a proxy of the property on a proxy object. In other words, a ModelProperty on a ModelItem. ModelItem does the work of implementing INotifyPropertyChanged while your runnable activity class itself does not implement this interface.

Fine in principle. Now dig down a little deeper. What really happens inside WPF when you bind to ModelItem.OwnerName? How does this text 'ModelItem.Owner' actually turn into a change notification listener, considering that a ModelItem object does not literally have any public property called OwnerName?

Here’s the basic things happening.

  1. With the syntax {Binding ModelItem.OwnerName}, a XAML parser sees “{Binding” and realizes it needs to process a XAML 'Binding' MarkupExtension.
  2. Binding class, which is a MarkupExtension (due to its attributes), decides how to process the markup extension (BindingBase initializes a Binding object with the PropertyPath we gave it, "ModelItem.OwnerName")
  3. The Binding results in the creation of a PropertyPath object, modelling the stepwise traversal of an object property graph
  4. The PropertyPath, when bound to an expression, adds event handlers to receive INotifyPropertyChanged from any objects in the property path. Additionally, whenever it is evaluated, the path is traversed, and the traversal is compatible with each ‘property name’ in the path being one of three things (based on inference from PropertyPath constructor(string, object[]) ) (in unknown order of preference):
    -a) a real object property as in System.Reflection.PropertyInfo from the object’s Type
    -b) a dependency property, found by the property Name
    -c) a PropertyDescriptor from a TypeDescriptor of the object - or the object itself if it implements ICustomTypeDescriptor.

[Note: Possibly WPF might also support dynamic (C# 4.0) property access to an IDynamicMetadataObjectProvider? I haven't checked this yet.]

Now presumably doing a binding on ModelItem has to work by one of these standard mechanisms that WPF uses. If ModelItem plays really nice we can expect it might allow us to use the same full set of mechanisms for representing the property on the object it wraps, and consistently surface them through to the PropertyPath targetting it just as if we were using a PropertyPath on the underlying object – just with the addition of change notification.

 

Peeking at ModelItemImpl

If you grab a ModelItem instance from your custom ActivityDesigner, I think you’ll consistently find that what you’ve actually got is a ModelItemImpl. And if you check out its interfaces, you can find that even though it is of an internal type, it has two public interfaces on it, ICustomTypeDescriptor and IDynamicMetadataObjectProvider, both of which, interestingly, appeared above.

Practically, which interfaces should it use to provide the properties to PropertyPath? For reasons of precedence, the best answer is to implement ICustomTypeDescriptor and in particular, ICustomTypeDescriptor.GetProperties() .

 

Using a Custom Type Descriptor with System.Activities.Presentation.ModelItem

If ModelItemImpl is going to play nice, and allow your WPF bindings to work consistently as they would have with the original objet, then it should try to respect the behavior of the object that it wraps, including where that object itself implements ICustomTypeDescriptor.

So let’s try it out by implementing ICustomTypeDescriptor.

public class MyCustomTypeDescriptorActivity : CodeActivity, ICustomTypeDescriptor

{ …

 

 

What’s a reasonable default behavior for the ICustomTypeDescriptor methods? Well, bear in mind that I’m just trying this out for the first time, but I think the reasonable default implementation for ICustomTypeDescriptor methods could look like this:

    public AttributeCollection GetAttributes()

    {

        return TypeDescriptor.GetProvider(this) .GetTypeDescriptor(this) .GetAttributes();

    }

 

In order for us to ‘add’ a property to our object via Custom Type Descriptor, we will need to deviate from the above template in GetProperties(). (And possibly, GetProperties(Attribute[] attributes).)

   public PropertyDescriptorCollection GetProperties()

    {

    // Get default property descriptors

    PropertyDescriptorCollection r = TypeDescriptor.GetProvider(this)

    .GetTypeDescriptor(this)

    .GetProperties();

 

        // Copy them to an array

        int n = r.Count;

        PropertyDescriptor[] arr = new PropertyDescriptor[n + 1];

        r.CopyTo(arr, 0);

 

        // Add a new custom property descriptor to array

        SimplePropertyDescriptor pd = new SimplePropertyDescriptor();

        arr[n] = pd;

 

        // Return a new PropertyDescriptorCollection

        var ret = new PropertyDescriptorCollection(arr);

   return ret;

    }

 

Lastly, we need to implement our own PropertyDescriptor type, SimplePropertyDescriptor.

    public class SimplePropertyDescriptor : PropertyDescriptor

    {

        public SimplePropertyDescriptor()

            : base("SimplePropertyDescriptor", new Attribute[0])

        {

        }

 

        public override bool CanResetValue(object component)

        {

            return false;

        }

 

        public override Type ComponentType

        {

            get { return typeof(MyCustomTypeDescriptorActivity); }

        }

 

        public override bool IsBrowsable

        {

            get { return true; }

        }

 

        public override Type PropertyType

        {

            get { return typeof(string); }

        }

 

        public override object GetValue(object component)

        {

            return "Hello";

        }

 

        public override bool IsReadOnly

        {

            get { return false; }

        }

 

        public override void ResetValue(object component)

        {

        }

 

        public override void SetValue(object component, object value)

        {

            throw new NotImplementedException();

        }

 

        public override bool SupportsChangeEvents

        {

            get

            {

                return false;

            }

        }

 

        public override bool ShouldSerializeValue(object component)

        {

            return false;

        }

    }

 

 

Does it work?

Well, I’m happy to report that yes, ModelItem detects and interoperates with our property descriptor. We can see it in the property grid. And also use it with a ModelItem Binding expression. Smile

The rehosted property grid

Notes:

If you want your property to show up in the Workflow Designer’s property grid, there’s a couple things you need to do exactly right:

  1. Set IsBrowsable = true – actually I’m just going to say this is necessary in theory. It turns out that Workflow Designer doesn’t check this value right now, but I believe it probably should.
  2. Set IsReadOnly = false – read-only properties (e.g. with no set accessor) never show up in the Workflow Designer property grid.

You can workaround the issue of IsBrowsable = false being ignored by adding a BrowsableAttribute(false) on your custom property descriptor (in the attribute array, in the call to the base constructor).

Comments

  • Anonymous
    March 02, 2015
    Could you tell if it is possible that the custom property is serializable in the XAML representation of the activity? I used this approach, and when opening the activity with View Code approach, the custom property is not serialized. Thank you.