Sunday, 12 October 2008

Dynamic Data – Custom Metadata Providers *** UPDATED 20081110 ***

This is really just an addition to Matt Berseth's article Dynamic Data And Custom Metadata Providers from August 24, 2008, all I wanted to do was add the same features to the Table/Class not just the Columns/Properties. So you can see the full explanation over at Matt Berseth's blog. So here are the listings:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Web;

public class DefaultAttributesTypeDescriptionProvider : System.ComponentModel.TypeDescriptionProvider
{
    private Type Type { get; set; }

    public DefaultAttributesTypeDescriptionProvider(Type type)
        : this(type, TypeDescriptor.GetProvider(type))
    {
        this.Type = type;
    }

    public DefaultAttributesTypeDescriptionProvider(Type type, TypeDescriptionProvider parentProvider)
        : base(parentProvider)
    {
        this.Type = type;
    }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        return new DefaultAttributesTypeDescriptor(base.GetTypeDescriptor(objectType, instance), this.Type);
    }
}

Listing 1 – TypeDescriptorProvider

No change in Listing 1 from what Matt has already done just some renaming to make it a bit clearer to me what's going on.

using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

public class DefaultAttributesTypeDescriptor : CustomTypeDescriptor
{
    private Type Type { get; set; }

    public DefaultAttributesTypeDescriptor(ICustomTypeDescriptor parent, Type type)
        : base(parent)
    {
        this.Type = type;
    }

    /// <summary>
    /// Returns the collection of attributes for a given table
    /// </summary>
    /// <returns>AttributeCollection</returns>
    public override AttributeCollection GetAttributes()
    {
        AttributeCollection baseAttributes = base.GetAttributes();

        if (baseAttributes.OfType<DisplayNameAttribute>().FirstOrDefault() == null)
        {
            List<Attribute> extraAttributes = new List<Attribute>();

            // generate the display name
            String friendlyDisplayName = base.GetClassName().ToTitleFromPascal();

            // add it to the list
            extraAttributes.Add(new DisplayNameAttribute(friendlyDisplayName));

            // only create a new collection if it is necessary
            return AttributeCollection.FromExisting(baseAttributes, extraAttributes.ToArray());
        }
        else
        {
            return baseAttributes;
        }
    }

    /// <summary>
    /// Returns a collection of properties (columns) for the type,
    /// each with attributes for that table
    /// </summary>
    /// <returns>PropertyDescriptorCollection</returns>
    public override PropertyDescriptorCollection GetProperties()
    {
        List<PropertyDescriptor> propertyDescriptors = new List<PropertyDescriptor>();

        foreach (PropertyDescriptor propDescriptor in base.GetProperties())
        {
            List<Attribute> newAttributes = new List<Attribute>();

            // Display Name Rules ...
            // If the property doesn't already have a DisplayNameAttribute defined
            // go ahead and auto-generate one based on the property name
            if (!propDescriptor.HasAttribute<DisplayNameAttribute>())
            {
                // generate the display name
                String friendlyDisplayName = propDescriptor.Name.ToTitleFromPascal();

                // add it to the list
                newAttributes.Add(new DisplayNameAttribute(friendlyDisplayName));
            }

            // Display Format Rules ...
            // If the property doesn't already have a DisplayFormatAttribute defined
            // go ahead and auto-generate one based on the property type
            if (!propDescriptor.HasAttribute<DisplayFormatAttribute>())
            {
                // get the default format for the property type
                String displayFormat = propDescriptor.PropertyType.GetDisplayFormat();

                // add it to the list
                newAttributes.Add(new DisplayFormatAttribute() { DataFormatString = displayFormat });
            }

            propertyDescriptors.Add(new WrappedPropertyDescriptor(propDescriptor, newAttributes.ToArray()));
        }

        // return the descriptor collection
        return new PropertyDescriptorCollection(propertyDescriptors.ToArray(), true);
    }

    private class WrappedPropertyDescriptor : PropertyDescriptor
    {
        private PropertyDescriptor _wrappedPropertyDescriptor;

        public WrappedPropertyDescriptor(PropertyDescriptor wrappedPropertyDescriptor, Attribute[] attributes)
            : base(wrappedPropertyDescriptor, attributes)
        {
            _wrappedPropertyDescriptor = wrappedPropertyDescriptor;
        }

        public override bool CanResetValue(object component)
        {
            return _wrappedPropertyDescriptor.CanResetValue(component);
        }

        public override Type ComponentType
        {
            get { return _wrappedPropertyDescriptor.ComponentType; }
        }

        public override object GetValue(object component)
        {
            return _wrappedPropertyDescriptor.GetValue(component);
        }

        public override bool IsReadOnly
        {
            get { return _wrappedPropertyDescriptor.IsReadOnly; }
        }

        public override Type PropertyType
        {
            get { return _wrappedPropertyDescriptor.PropertyType; }
        }

        public override void ResetValue(object component)
        {
            _wrappedPropertyDescriptor.ResetValue(component);
        }

        public override void SetValue(object component, object value)
        {
            _wrappedPropertyDescriptor.SetValue(component, value);
        }

        public override bool ShouldSerializeValue(object component)
        {
            return _wrappedPropertyDescriptor.ShouldSerializeValue(component);
        }
    }
}

Listing 2 – CustomTypeDescriptor

Here all I’ve done is again some renaming and add a GetAttributes method which returns the attributes on the class/table. The section is marked in BOLD ITALIC for emphasis. What it does is check to see if the DisplayNameAttriburte is set and if not then auto generates it.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.DynamicData;
using Microsoft.Web.DynamicData;

public static class HelperExtansionMethods
{
    public static ColumnOrderAttribute ColumnOrdering(this MetaColumn column)
    {
        return column.Attributes.OfType<ColumnOrderAttribute>().DefaultIfEmpty(ColumnOrderAttribute.Default).First();
    }

    public static Boolean IsHidden(this MetaColumn column, PageTemplate currentPage)
    {
        var hideIn = column.Attributes.OfType<HideColumnInAttribute>().DefaultIfEmpty(new HideColumnInAttribute()).First() as HideColumnInAttribute;
        return hideIn.PageTemplates.Contains(currentPage);
    }

    /// <summary>
    /// Converts a Pascal type Name to Title
    /// i.e. MyEmployees becomes My Employees
    ///      OrderID becomes Order ID etc
    /// </summary>
    /// <param name="s">String to convert</param>
    /// <returns>Title String</returns>
    public static String ToTitleFromPascal(this String s)
    {
        // remove name space
        String s0 = Regex.Replace(s, "(.*\\.)(.*)", "$2");
// add space before Capital letter String s1 = Regex.Replace(s0, "[A-Z]", " $&"); // replace '_' with space String s2 = Regex.Replace(s1, "[_]", " "); // replace double space with single space String s3 = Regex.Replace(s2, " ", " "); // remove and double capitals with inserted space String s4 = Regex.Replace(s3, "(?<before>[A-Z])\\s(?<after>[A-Z])", "${before}${after}"); // remove and double capitals with inserted space String sf = Regex.Replace(s4, "^\\s", ""); // force first character to upper case return sf.ToTitleCase(); } public static String ToTitleCase(this String text) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.Length; i++) { if (i > 0) { if (text.Substring(i - 1, 1) == " " || text.Substring(i - 1, 1) == "\t" || text.Substring(i - 1, 1) == "/") sb.Append(text.Substring(i, 1).ToString().ToUpper()); else sb.Append(text.Substring(i, 1).ToString().ToLower()); } else sb.Append(text.Substring(i, 1).ToString().ToUpper()); } return sb.ToString(); } public static Boolean HasAttribute<T>(this PropertyDescriptor descriptor) where T : Attribute { Boolean value = false; for (int i = 0; i < descriptor.Attributes.Count && !value; i++) { value = (descriptor.Attributes[i] is T); } return value; } public static String GetDisplayFormat(this Type type) { string defaultFormat = "{0}"; if (type == typeof(DateTime) || type == typeof(Nullable<DateTime>)) { defaultFormat = "{0:d}"; } else if (type == typeof(decimal) || type == typeof(Nullable<decimal>)) { defaultFormat = "{0:c}"; } return defaultFormat; } }
Listing 3 – Helper extension methods 
UPDATED: 2008/11/10 altered ToTitleFromPascal extension method to remove namespace and to force to title case in cases where Table or column name is:
EntitiesModel.MyEntity
or
tbl_name_of_table

I’ve moved my helper extension methods out of the class and rewritten the ToHumanFromPascal as ToTitleFromPascal and used Regex to do my converting.

I hope Matt doesn't mind. smile_teeth

15 comments:

Mark said...

I get an error on the line:

if (baseAttributes.OfType<DisplayNameAttribute>().FirstOrDefault() == null)

'System.ComponentModel.AttributeCollection' does not contain a definition for 'OfType' and no extension method 'OfType' accepting a first argument of type 'System.ComponentModel.AttributeCollection' could be found (are you missing a using directive or an assembly reference?)

What am I missing?

- Mark

Steve said...

Hi Mark I think you will need a using System.Linq; in there :D

Steve

Mark said...

That took care of it. Thanks!

Jason Kergosien said...

Steve,

I like where you're going on this.

How would one go about creating providers for the other attribute types (ex. Range, UIHint, Filter, etc.)?

Steve said...

Hi Jason, I'm not sure what you mean? do you mean automating e.g.Range, UIHint, Filter, etc if so how would you go about deciding what UIHine to do on the field?

Again you could do what your asking, and the only issue is deciding how to detect which attribute to add. :D

Steve

David said...

Thx.
I follow your tutorial but I have this error :
The call is ambiguous between the following methods or properties:

'ExtensionMethods.HasAttribute[System.ComponentModel.DisplayNameAttribute]System.ComponentModel.PropertyDescriptor)'

and

'ExtensionMethods.HasAttribute[System.ComponentModel.DisplayNameAttribute](System.ComponentModel.PropertyDescriptor)'

As you can see, there are the same ! I red something here which help me a little but I still have the problem :
http://stackoverflow.com/questions/398037/asp-net-web-site-or-web-application

(ps : I replace '<' by '[' in the error)
Thx Steve.

David said...

Ok, sorry for asking too fast.
I solved the problem by moving each extended method into the coresponding file.
I just need to declare the ExtensionMethods class as partial.
Thx again.

Anonymous said...

I'm a bit confused what the advantage of the new GetAttributes method is? Sorry, new to Dynamic Data.

Steve said...

Hi, I find that it makes my code more readable.

Steve :D

Xaeryan said...

Is there any way to get access to the entity key in these methods? In particular, in the GetProperties method, if I can access the entity key, I have a lookup table that will be used to provide the descriptions, as a simple regex replacement won't always fit the bill. Thanks!

Steve said...

Hi Xaeryan, do you want to send me an e-mail and I can find out in more detail what it is you want to do please.

Steve :D

Dwaine said...

Steve,

I started with matt's version and had it working. trying to get your version working, but there's no Microsoft.Web.DynamicData; namespace available in my environment.
Am I missing something?

Steve said...

I suspect that is just a hold over on some class file I was reusing or I copied it from the usings of a similar class. I don't think it should be required but it may if you want it download the Dynamic Data Futures from Codeplex.com and use the futures project dll, but you will find if I am using it it will just be some of the extension methods.

Steve :D

Kevin said...

Steve,

The TypeDescriptionProvider seems to ignore any metadata added to the Globals.asax.cs file. Is this the intended functionality?

- Kevin

Steve said...

Hi Kevin, I'm not sure what you mean you don't add Metadata in Glabal.asax.cs using this?

Steve