Friday, June 25, 2010

Using DataAnnotations on Generated Entities within Silverlight

It makes sense to use DataAnnotations to decorate your models so that when you bind those models to the DataForm within the Silverlight Toolkit, that DataForm comes to life with all the proper validation and display functionality.  To add the data annotations you need to decorate properties like:

[Required(ErrorMessage="Date is a required field.")]
[Display(Name="Date", Description="Date of event.")]
[DisplayFormat(DataFormatString="{0:d}")]
public DateTime Date { get; set; }

 

This works well if you create your models a POCO’s within your Silverlight project.  However, in most cases you will want to bind to data that is presented through some sort of service.  If you add a service to your project, a proxy will be generated that really can’t be, shouldn’t be modified.  The challenge is when your proxy for the service gets generated the property is generated as well, any modifications you make to that generated property will be overwritten if you regenerate your proxy.

In the MVC2 world, you can use the MetadataType attribute to decorate a partial class for the generated entity within your proxy.  In the case below I am mapping the meta data class MyNameSpace.Day.MetaData to the generated entity MyNameSpace.Day via a partial class.:

using System;
using System.ComponentModel.DataAnnotations;

namespace MyNamespace
{
    [MetadataType(typeof(MetaData))]
    public partial class Day
    {
        public class MetaData
        {
            [Required(ErrorMessage="Date is a required field.")]
            [Display(Name="Date", Description="Date of event.")]
            [DisplayFormat(DataFormatString="{0:d}")]
            public object Date { get; set; }

            [Required()]
            [Display(Name="Display Date",Description="Display.")]
            public object DateDisplay { get; set; }

        }
    }
}

However sadly as of Silverlight 4, the MetadataType attribute does not exist within System.ComponentModel.DataAnnotations namespace.  The solution I came up with was to provide my own implementation of MetadataType.  I reused the System.ComponentModel.DataAnnotations namespace for two reasons.  First because when the functionality does indeed become available within Silverlight your code won’t need to change.  Second you compiler will tell you with an error when it does become available, at that point you can use the standard implementation. 

Here’s my very basic implementation:

using System;

namespace System.ComponentModel.DataAnnotations
{
    public class MetadataTypeAttribute : Attribute
    {
        public MetadataTypeAttribute(Type t)
        {
            MetaDataType = t;
        }

        public Type MetaDataType
        {
            get;
            set;
        }
    }
}

With the above implementation, my partial class that uses the MetadataType object will compile with Silverlight as it did with MVC2.

The final component to make this work is to let the DataForm know how to look for the MetaData.  This requires building the DataForm within the Silverlight toolkit and modifying the code.  I’m sure you realize the risk in doing something like this, but hopefully in a future release of the DataForm this functionality will be baked in and you can just use the canned version and the rest of your application should work without any modifications.

Within the System.Windows.Controls.Data.DataForm.Toolkit project you should look for /DataField/DataField.cs this is where all the magic happens to add the validation and display functionality to you form.  Specifically look for a method GetPropertyInfo(), you will modify the functionality of that method to check to see if the class that is mapped to the form as the custom attribute MetadataTypeAttribute.  If so it will use the class associated with it to look for decorated properties used to properly render the form.  Since GetPropertyInfo() is used by all the methods to render the form, this one change supports all the different types of attributes used for form validation and display.

private PropertyInfo GetPropertyInfo()
{
    Debug.Assert(this.DataContext != null, "DataContext should never be null
                                       when GetPropertyInfo() is called.");

    PropertyInfo propertyInfo = null;

    if (!string.IsNullOrEmpty(this.PropertyPath))
        propertyInfo = this.DataContext.GetType().GetPropertyInfo
                                                   (this.PropertyPath);

    if (propertyInfo == null && this.Content != null)
    {
        var bindingInfos = this.Content.GetDataFormBindingInfo(
                                 this.DataContext, 
                                 false /* twoWayOnly */,
                                 false /* searchChildren */);

        foreach (DataFormBindingInfo bindingInfo in bindingInfos)
        {
            var binding = bindingInfo.BindingExpression.ParentBinding;

            if (binding != null &&
                binding.Path != null &&
                !string.IsNullOrEmpty(binding.Path.Path))
            {
                var contextType = this.DataContext.GetType();
                var bindingPath = binding.Path.Path;
                var metaDataTypes = contextType.GetCustomAttributes(typeof
                                        (MetadataTypeAttribute), false);
                if (metaDataTypes.Length == 1)
                {
                    var metaDataTypeInfo = metaDataTypes[0] as
                                                  MetadataTypeAttribute;
                    var metaDataType = metaDataTypeInfo.MetaDataType;
                    propertyInfo = metaDataType.GetPropertyInfo(bindingPath) 
                                                  as PropertyInfo;

                    if (propertyInfo != null)
                        break;
                 }
                else
                {
                    propertyInfo = contextType.GetPropertyInfo(bindingPath)
                                                  as PropertyInfo;

                    if (propertyInfo != null)
                        break;
                }
            }
        }
    }

    return propertyInfo;
}

Ever since I started using code generation to create a DataAccessLayer for my application, I’ve dreamt of the day I could decorate my models with attributes, then anywhere I needed to expose those models to the UI, the UI would just pickup those attributes and behavior properly.  The above approach is somewhat “hack-ish” but does provide a stop-gap until a future generation of Silverlight implements this functionality.  It also does so in such a away that should be forward-compatible, at least as much as possible without a crystal ball. 

-twb

4 comments:

  1. Hi, i like your solution and i'm using it.
    I need to hide some fields in my UI model (mapped from the WCF entities with AutoMapper) so i refined your patch by modifying a method in DataForm.cs too.
    I patched GenerateFields() method so it can correctly manage the Display( AutoGenerateField=false) attribute parameter.

    Here is the patched method:

    http://pastebin.com/jTA27Xqq

    ReplyDelete
  2. Kevin -
    Thanks so much for this! Great idea. It's really too bad that Silverlight 4 doesn't have this stuff built in. This issue is in the Silverlight wishlist, so I would suggest that everyone vote on it. Here's the URL: http://dotnet.uservoice.com/forums/4325-silverlight-feature-suggestions/suggestions/997281-support-metadatatype-attribute?ref=title.

    -Damien

    ReplyDelete
  3. Hi Kevin,
    I have found out a way of validating DTOs client side in my Silverlight app. Can you please have a look and comment:

    http://silverlight-dto-validation.blogspot.com/

    Thanks,
    Dharmesh

    ReplyDelete
  4. Hi kevin,

    I have a silverlight business application.
    Where can I find the GetPropertyInfo() that I am suppose to customize ?

    Jack

    ReplyDelete