次の方法で共有


Understanding the TypeDescriptor: A Metadata Engine for Designtime Code

Hi folks - long time no see...

E.L.D.S.? Yeah, sure! Never mind that, here is a dump of my understanding of
the TypeDescriptor. Hope you find it useful!

What is Metadata in the context of CLR? Read all about it

here. And what makes this concept really powerful is that any code can
access the CLR metadata engine (using

managed and

unmanaged API) to inspect any given managed code down to each IL
instruction. The CLR metadata engine also has API to let other code emit managed
code. But once emitted and baked, the CLR metadata engine will only let
you inspect the managed code – you will not be able to extend or modify any of
it.

Now we all know that the all-powerful and mighty .NET Framework Designtime
infrastructure lets every component have 2 views of itself – a designtime view
and a runtime view. You might have also observed that the designtime view is not
quiet the same as the runtime view. If you haven’t, consider the following:

  1. Every Form on the design surface of a Visual Studio
    2005 DeviceApplication project has a property called FormFactor. Can
    you locate it in System.Windows.Forms.dll using

    Reflector or

    ILDAsm or the

    Reflection API?

  2. If the

    ErrorProvider component is present on the design surface, all other
    controls on the design surface have new property called Error on
    errorProvider1
    . Can you locate this property in System.Windows.Forms.dll?

This means that the designtime infrastructure is displaying a metadata
different from what is returned by

reflection API. Clearly someone is modifying the metadata before the
designtime infrastructure displays it. Now who would that be?

The

System.ComponentModel.TypeDescriptor of course!

TypeDescriptor is a metadata engine provided by the .NET FCL. The

MSDN documentation says the following:

... In contrast, TypeDescriptor is an
extensible inspection mechanism for components: those classes that implement
the
IComponent interface...

That is not quiet correct. TypeDescriptor is an extensible inspection
mechanism not just for components but for all Types and for individual instances
of any given Type. Although if the target is a component, it may have a

Site and the Site may proffer services, e.g. the

ITypeDescriptorFilterService (discussed below) or the

IExtenderProvider, which can further enhance the extensibility of the
TypeDescriptor.

To restate: Unlike the native CLR metadata engine, TypeDescriptor lets
you inspect as well as modify the metadata (add, change and delete) of the
target in any conceivable manner. The term target, for the rest of this
text will refer to an element of the set of all .NET Framework Types (including
Types imported from COM) and all instances of every .NET Framework Type.

Note that an update to a Type’s metadata is visible for the Type and all its
instances. But when the metadata of an instance of any Type is updated, the
update is seen only for that instance. Metadata of the Type and its other
instances are unaffected.

Also note that TypeDescriptor and Reflection API do not interoperate. While
TypeDescriptor gets the initial metadata information using Reflection (explained
below), further changes to metadata using TypeDescriptor is not visible using
Reflection. Only the TypeDescriptor API will let you inspect the new metadata
and update it further.

Here is an example of metadata inspection using TypeDescriptor:

using System;
using System.ComponentModel;

namespace InspectWithTD

{

class Program

    {

        static void Main(string[] args)

{

Console.WriteLine("Attributes on System.Object are:");

AttributeCollection attributes = TypeDescriptor.GetAttributes(typeof(object));

foreach (Attribute attribute in attributes) Console.WriteLine("- " + attribute.GetType().Name);

Console.WriteLine("\nProperties of a string object are:");

PropertyDescriptorCollection properties = TypeDescriptor.GetProperties("A String");

foreach (PropertyDescriptor property in properties) Console.WriteLine("- " + property.Name);

Console.WriteLine("\nEvents on the AppDomain we are running in are:");

EventDescriptorCollection events = TypeDescriptor.GetEvents(AppDomain.CurrentDomain);

foreach (EventDescriptor @event in events) Console.WriteLine("- " + @event.Name);

}

}

}

In the following sections we will take a brief look at the architecture of
the TypeDescriptor. Then we see what its capabilities are. And finally close the
topic with an example. But
before we get started, I suggest you open System.dll in

Reflector, navigate to TypeDescriptor and disassemble the method
implementations to get a deeper understanding.

Brief Architecture of TypeDescriptor

Internally, the TypeDescriptor creates (on demand) and maintains a stack of

TypeDescriptionProvider (provider hereafter) for each target. Providers can
be considered as plugins for the TypeDescriptor and they are the entities
responsible for supplying the target’s metadata and for most other
TypeDescriptor magic.

For Type targets, the TypeDescriptor can initialize the provider stack with a
read-only provider that is built on

Reflection API. Or with a delegating provider that is hooked up to
delegate the calls each time to the latest provider for the Type’s base Type.
This way the calls are handled by the latest provider for the Type’s base Type,
if the Type doesn’t have provider stack by itself.

For instance targets, the provider stack can be the same as the provider
stack of the instance’s Type. Or it can be initialized with a delegating
provider that is hooked up to delegate the calls each time to the latest
provider for the instance’s Type. This way the calls are handled by the latest
provider for the instance’s Type, if the instance doesn’t have provider stack by
itself.

Once the provider stack is initialized, the TypeDescriptor lets you
add/remove custom providers to/from the stack. Custom providers need to extend

TypeDescriptionProvider and can have a parent provider. They can selectively
change the TypeDescriptor behavior for a given target by selectively overriding
the TypeDescriptionProvider methods. If a parent provider exists, the methods of
TypeDescriptionProvider delegate the work to the parent provider. This is how we
achieve TypeDescriptionProvider chaining. The provider one on the top of
the target’s provider stack (latest provider hereafter) is returned by

TypeDescriptor.GetProvider and is the one doing most of the TypeDescriptor’s
work as we are going to see below.

To proceed further we must understand the

ICustomTypeDescriptor (custom type descriptor or CTD hereafter). Click on
the link and see its API – isn’t there a striking similarity with its methods
and metadata inspection methods of the TypeDescriptor? Indeed – the purpose of a
CTD is to return the metadata for the given target. The default implementation
of ICustomTypeDescriptor, the

CustomTypeDescriptor, takes in a parent ICustomTypeDescriptor (to enable
CustomTypeDescriptor chaining) in its constructor. If the parent exists,
each method of the CustomTypeDescriptor simply delegates the call to the parent
– else the methods return empty metadata.

TypeDescriptionProvider.GetTypeDescriptor returns the CTD supplied by a
target’s provider. For instance targets another method,

TypeDescriptionProvide.GetExtendedTypeDescriptor also returns a CTD. But
this CTD implementation returns the metadata contributed to the instance by the

Extender Providers. The default implementation of this returns empty
metadata if the instance is not a component or it is not sited. Otherwise if the
target is sited and the site proffers the

IExtenderListService, which lists the extenders. If the site doesn’t proffer
the service, the extenders in the target’s container are listed. From the list
of extenders, the extended metadata is collected.

The TypeDescriptor methods that return the target’s metadata are in fact
wrappers around the corresponding methods of the CTD obtained from the
target’s latest provider. For Types, calling GetTypeDescriptor on the latest
provider obtains the CTD. However obtaining the CTD for an
instance is a little involved since the instance itself may implement
ICustomTypeDescriptor. If it does, then the CTD returned is a merge of the
ICustomTypeDescriptor implemented by the instance and the CTD supplied by the
latest provider of the instance – but with a preference to the
ICustomTypeDescriptor implemented by the instance. If the target doesn’t
implement ICustomTypeDescriptor then the CTD obtained is the one supplied
by the instance’s latest provider. This means any arbitrary object, by
implementing ICustomTypeDescriptor, can dynamically modify the metadata obtained
from it by the TypeDescriptor.
Of course many of the TypeDescriptor method
take in a boolean argument – which when true, simply tells the TypeDescriptor to
ignore the instance’s implementation of ICustomTypeDescriptor.

A few of the above methods that return metadata of instances are a bit more
sophisticated. For instance the

TypeDescriptor.GetProperties that takes in an instance, determines the set
of properties to return in as many as 4 successive stages. The output of each
stage is fed to the next stage.

  1. First stage: The CTD is obtained (as above)
    from the latest provider and it returns the initial set of properties.

  2. Second stage: The input set is merged with the set of
    properties contributed by the Extender Providers. To obtain this set,
    GetExtendedTypeDescriptor is called on the latest provider for the instance.

  3. Third stage: If the component is sited and the site
    proffers the

    ITypeDescriptorFilterService, the input set is passed to the
    ITypeDescriptorFilterService to be filtered. This forms the basis of the

    IDesignerFilter that allows a

    ComponentDesigner to add/change/delete properties from the set of
    properties of the component it is designing.

  4. Fourth stage: The input set is filtered based on a set
    of attributes. The rules used for this filtering is explained well in the
    Remarks section

    here.

Whew! That is sophistication for you. The power you wield is just awesome!
The above is for GetProperties method with instance target. The logic is similar
for

TypeDescriptor.GetAttributes and

TypeDescriptor.GetEvents methods with instance target. Of course, the
metadata obtained from the above stages are cached into the

IDictionaryService if the instance happens to be a component and its site
proffers the dictionary service. This way, unless the cache is invalid, the
subsequent call to obtain the metadata can work off the cache. Note that this is
a per-instance cache, unlike the per-Type cache used by TypeDescriptor methods
that work on Types. The necessity of a per-object cache is explained well in the
Remarks section

here. To obtain the per-instance cache, call the

GetCache on the latest provider. To clear the per-type and the per-object
caches call

TypeDescriptor.Refresh. Once the caches are cleared, the TypeDescriptor will
rebuild all of them on subsequent calls.

We have seen how we can write a TypeDescriptionProvider and a
CustomTypeDescriptor for a given target and return any metadata we choose – say
a completely different set from what is returned by Reflection. Now, what if
someone wants to call some of the lower level Reflection API on the target to
get more information about the target? E.g. say the target is object1 and using
TypeDescriptor.GetProperties(object1) we get the Pubic, DeclaredOnly
and Instance properties of object1. This set of properties is different
from the set obtained by object1.GetType().GetProperties() with the above

BindingFlags. We have an inconsistency a.k.a. confusion and this is not a
happy situation!  To avoid such scenarios, you should use

TypeDescriptor.GetReflectionType(object1).GetProperties() with the above
BindingFlags. Internally this method will return the System.Type implementation
returned by the

GetReflectionType call on the object1’s provider. So if you are greatly
mucking around with the metadata, make sure your provider’s GetReflectionType
returns a System.Type implementation that returns metadata consistent with the
provider’s custom type descriptor. This is exactly what happens in the VSD
designers where we are actually using a desktop type (e.g. desktop WinForms

Button) to represent the corresponding device type (the NETCF

Button) on the design surface. We will delve deeper into this in a future
post. Also for another illustration, refer to Brian Pepin’s post on

designing abstract forms.

As if this much power isn’t enough, the

TypeDescriptor.CreateInstance, takes all of the above a step further.
CreateInstance first checks if the ServiceProvider (first argument) proffers a
TypeDescriptionProvider and if so, it delegates the call to this provider. Else
the provider for the target is located the call is delegated to its

CreateInstance. The provider can create and return any instance of any other
Type (this is called Instance Substitution). Refer to Brian’s post
mentioned above to see application of this. Another application of this is in
the VSD designers, where we implement a provider that overrides the
CreateInstance calls to hook in Type Filtering into the designtime – this is the
trick that filters a desktop Type such that on the design surface it
appears as the corresponding device (.NETCF) Type.  More about these in the same
future post I promised above.

Capabilities of the TypeDescriptor

The purpose of all of the above blah, blah, yada, yada... is so that you can
better understand the capabilities of the TypeDescriptor. A great set of
documentation on is already provided on

MSDN2 that lists and explains all of them – so read it carefully and
understand it and let me know if some parts are still not clear. Understanding
the TypeDescriptor is critical to understanding my future posts on Designers. To
restate, the following are the capabilities of the TypeDescriptor:

Capability

Description

Instance substitution

Enables an arbitrary type to be created when another type is requested.

Metadata substitution

Enables an object's metadata to be modified.

Attribute redirection

Enables attributes to be specified dynamically.

Target substitution and shadowing

Enables one object to stand in for another.

Extended type descriptor support

Enables access to object properties added by other objects.

A simple illustration...

Let us now close this discussion with a small example. It doesn’t do anything
useful other than illustrating that you can add your choice of metadata to any
given .NET object/Type. We ask TypeDescriptor to register a custom provider the System.Object. And from the above you can see why this provider automatically
becomes the provider for all .NET objects and Types. Our custom descriptor than
returns a custom type descriptor which when asked for properties, just creates a
custom property and returns it along with the original set of properties. Note
that both our custom provider and type descriptor selectively override 1 method
each and using chaining they delegate the remaining calls to their respective
parents.

using System;

using System.ComponentModel;

using System.Collections.Generic;

namespace ExtendWithTD

{

class
Program
   
{

static
void Main(string[]
args)

{

Console.WriteLine("Properties
of an arbitrary object: Console.Out:");

PropertyDescriptorCollection
properties =
TypeDescriptor.GetProperties(Console.Out);

foreach (PropertyDescriptor
property in
properties) Console.WriteLine("-
" +
property.Name);

Console.WriteLine("\nAdd
our useless provider System.Object and chain it with the original one...");

TypeDescriptor.AddProvider(new UselessTypeDescriptionProvider(TypeDescriptor.GetProvider(typeof(object))),
typeof(object));

Console.WriteLine("\nProperties
of an arbitrary object: Console.Out:");

properties = TypeDescriptor.GetProperties(Console.Out);

foreach (PropertyDescriptor
property in
properties) Console.WriteLine("-
" +
property.Name);

}

}

/// <summary>
   
///
This is our custom provider. It simply provides a custom type descriptor
   
///
and delegates all its other tasks to its parent
   
/// </summary>
   
internal
sealed
class
UselessTypeDescriptionProvider
: TypeDescriptionProvider
   
{

/// <summary>

   
///
Constructor
       
/// </summary>
       
internal
UselessTypeDescriptionProvider(TypeDescriptionProvider
parent)

: base(parent)

{

}

/// <summary>
       
///
Create and return our custom type descriptor and chain it with the original

   
///
custom type descriptor

   
/// </summary>

   
public
override
ICustomTypeDescriptor
GetTypeDescriptor(Type
objectType, object
instance)

{

return
new
UselessCustomTypeDescriptor(base.GetTypeDescriptor(objectType,
instance));

}

}

/// <summary>

///
This is our custom type descriptor. It creates a new property and returns it
along

///
with the original list

/// </summary>

internal
sealed
class
UselessCustomTypeDescriptor
: CustomTypeDescriptor

{

/// <summary>

///
Constructor

/// </summary>

internal
UselessCustomTypeDescriptor(ICustomTypeDescriptor
parent)

: base(parent)

{

}

/// <summary>
       
///
This method add a new property to the original collection
       
/// </summary>
       
public
override
PropertyDescriptorCollection
GetProperties()

{

// Enumerate the original set of
properties and create our new set with it

       
PropertyDescriptorCollection
originalProperties = base.GetProperties();

List<PropertyDescriptor>
newProperties = new
List<PropertyDescriptor>();

foreach (PropertyDescriptor
pd in
originalProperties) newProperties.Add(pd);

// Create a new property and add it to
the collection

       
PropertyDescriptor
newProperty =
TypeDescriptor.CreateProperty(typeof(object),
"UselessProperty",
typeof(string));

newProperties.Add(newProperty);

// Finally return the list

       
return
new
PropertyDescriptorCollection(newProperties.ToArray(),
true);

}

}

}

There is obviously the inconsistency I mentioned above with this example.
Ideally, we should implement a System.Type and the custom type descriptor’s
GetProperties method should be using our System.Type implementation. But hey! As
an exercise, why don’t you come up with the implementation?

Let me know if something isn’t clear from above. I will be glad to explain
further...

See you soon in my next post on VSD designers...

Comments

  • Anonymous
    January 04, 2006
    What a long post! should take a couple of readings to grok this :)

  • Anonymous
    January 05, 2006
    The comment has been removed

  • Anonymous
    February 09, 2006
    Can you show how to route a CF DLL's metadata to its corresponding .asmmeta.dll? It is a very good example to demo TypeDescriptionProvider.

  • Anonymous
    March 08, 2006
    The comment has been removed

  • Anonymous
    March 12, 2007
    How to make the type descriptor persistent. how to store/reuse the created property through type descriptor. how to use created property after closing and reopening the application?

  • Anonymous
    September 27, 2007
    Hi Partho, Great article on this tricky subject... Can I ask you a tricky question in relation to it ? Do you know how to get to the implementing class of the current typedescriptor node for a type ? I suspect this is difficult/impossible because TypeDescriptor appears to use delegates to call the relevant methods on the active type descriptor; meaning the instance returned from GetTypeDescriptor((typeof(MyType)) is not an instance of the CTD but some sort of wrapper around it. Am I correct ? I can test this by implementing an interface on the CTD. For example...

  • My CTD implements an interface but when I ask for the currently active instance of the TypeDescriptor the result does not implement it but calling the methods on it works fine.... public class MyCTD : CustomTypeDescriptor, ISomeInterface { override public getproperties() { .... } } { TypeDescriptorProvider provider = TypeDescritor.GetProvider(typeof(MyType)); ICustomTypeDescriptor ctd = provider.GetTypeDescriptor(typeof(MyType)); ISomeInterface si = (ctd as ISomeInterface);          // always null...... ctd.GetProperties() // calls the correct method in my CTD } Any ideas ? Thanks