Tuesday, 19 January 2010

Page Action Mapping in Dynamic Data

Well the idea here save me creating a new route each time I decide that a certain table should use a different PageTemplate than normal.

Lets say I have these tables (Northwind well when I used AdventureWorks people said I don’t have that can you do a sample with Northwind)

  • Categories
  • CustomerCustomerDemo
  • CustomerDemographics
  • Customers
  • Employees
  • EmployeeTerritories
  • Order Details
  • Orders
  • Products
  • Region
  • Shippers
  • Suppliers
  • Territories

Normally all of these would have a standard list page:

ListPage

Figure 1 – Default List page.

but lets say I want these tables

  • Categories
  • Order Details
  • Region
  • Shippers
  • Suppliers

to use the ListDetails page then I’ve go to enter a custom route for each table; what I propose to do is add an attribute to each table in the metadata and have it done automatically for me and without those extra routes.

The Attribute

What I want is to be able to say is, if this Action or this or this the match it with this e.g. if List or Details then substitute ListDetails i.e. just for the tables I want use the other default routing. Hope that makes sense Happy

[Flags]
public enum PageTemplate
{
    // standard page templates
    Details         = 0x01,
    Edit            = 0x02,
    Insert          = 0x04,
    List            = 0x08,
    ListDetails     = 0x10,
    // custom page templates
    Unknown         = 0xff,
}

Listing 1 – PageTemplate enum

I’m setting up my own enum mainly because the PageAction is not an enum and can’t be used like PageAction.List | PageAction.Details ordered together using the [Flags] attribute and also because I want to be able to add other page templates such as my UpdateableList which has inline editing an insert or one of my other custom PageTemplates.

[MetadataType(typeof(CategoryMetadata))]
[PageActionMapping(PageTemplate.List | PageTemplate.Details, "ListDetails")]
partial class Category
{
    partial class CategoryMetadata
    {
        public Object CategoryID { get; set; }
        public Object CategoryName { get; set; }
        public Object Description { get; set; }
        public Object Picture { get; set; }
        // Entity Set 
        public Object Products { get; set; }
    }
}

Listing 2 – example metadata using attribute

But I also want the Attribute to support more complex mappings like:

[PageActionMapping(
    PageTemplate.List | PageTemplate.Details, "ListDetails",
    PageTemplate.Insert | PageTemplate.Edit, "SpecialUpdatePage")]

Here I have two different mapping for the same table.

/// <summary>
/// Allows mapping of page action to other 
/// actions on a table by table basis
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class PageActionMappingAttribute : Attribute
{
    /// <summary>
    /// Gets or sets the action mapping parameters.
    /// </summary>
    /// <value>The mappings.</value>
    public Dictionary<String, String> MappingParameters { get; private set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="PageActionMappingAttribute"/> class.
    /// </summary>
    /// <param name="mappings">The mappings.</param>
    public PageActionMappingAttribute(params Object[] mappingParameters)
    {
        SetMappings(mappingParameters);
    }

    /// <summary>
    /// Converts the mapping parameters from an object array to a dictionary.
    /// </summary>
    /// <param name="mappingParameters">The mapping parameters.</param>
    private void SetMappings(Object[] mappingParameters)
    {
        // initialise dictionary
        MappingParameters = new Dictionary<String, String>();

        // make sure we some parameters
        if ((mappingParameters != null) && (mappingParameters.Length != 0))
        {
            // check that they are in pairs
            if ((mappingParameters.Length % 2) != 0)
                throw new ArgumentException(
                    "PageActionMappingAttribute mapping parameters needs an even number of prameters");

            // loop through each pair
            for (int i = 0; i < mappingParameters.Length; i += 2)
            {
                var keyObject = mappingParameters[i];
                var value = mappingParameters[i + 1];

                // check there is a valid key
                if (keyObject == null)
                    throw new ArgumentException("PageActionMappingAttribute a key is null");

                // check it is a PageTemplate
                if (!(keyObject is PageTemplate))
                    throw new ArgumentException("PageActionMappingAttribute keys must be a PageTemplate");

                // check value is of type string
                if (!(value is String))
                    throw new ArgumentException("PageActionMappingAttribute value must be a string");

                // get the key(s)
                var keys = ((PageTemplate)keyObject).ToString().Split(new String[] { ", " }, 
                    StringSplitOptions.RemoveEmptyEntries);

                // check to see if we have OR'ed enum values
                if (keys.Length > 1)
                {
                    // loop through each enum value
                    foreach (var key in keys)
                    {
                        // make sure we don't have any duplicates.
                        if (this.MappingParameters.ContainsKey(key))
                            throw new ArgumentException(String.Format(
                               "PageActionMappingAttribute has duplicate key:{0}", key));

                        // add entries to dictionary
                        this.MappingParameters.Add(key, (String)value);
                    }
                }
                else
                    // add entries to dictionary
                    this.MappingParameters.Add(keys[0], (String)value);
            }
        }
    }
}

Listing 3 – PageActionMappingAttribute

The only complicated part of the attribute is the SetMappings methods all this private method does is convert the Object array into a Dictionary<String, String>() and do a bit of validation of the parameters.

The Route Handler

The route handler inherits from the DynamicDataRouteHandler and then get the PageActionMappingAttribute and check for a mapping match by checking if the dictionary contains a key that matches the action and if so substitutes the current action for the mapped value.

/// <summary>
/// Does page action mapping on routes saving 
/// manually adding routes for custom page template.
/// </summary>
public class PageActionMappingRouteHandler : DynamicDataRouteHandler
{
    public PageActionMappingRouteHandler() { }

    /// <summary>
    /// Creates the handler.
    /// </summary>
    /// <param name="route">The route.</param>
    /// <param name="table">The table.</param>
    /// <param name="action">The action.</param>
    /// <returns>An IHttpHandler</returns>
    public override IHttpHandler CreateHandler(
        DynamicDataRoute route,
        MetaTable table,
        string action)
    {
        var httpContext = HttpContext.Current;
        if (httpContext != null && httpContext.User != null)
        {
            // get mappings
            var actionMappings = table.GetAttribute<PageActionMappingAttribute>();
            // apply substitute mapping
            if (actionMappings != null && 
                actionMappings.MappingParameters.ContainsKey(action))
                action = actionMappings.MappingParameters[action];

            // return route from base
            return base.CreateHandler(route, table, action);
        }
        // return null in no context
        return null;
    }
}

Listing 4 – PageActionMappingAttribute

Putting it Together

Now all we need to do is add the route handler to the Routes in the Global.asax file.

routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
    RouteHandler = new PageActionMappingRouteHandler(),
    Model = model
});

Listing 5 – Default route with route handler

all we need to do is assign our route handler in each route in the Global.asax file see Listing 5 and add our metadata see Listing 2.

Downloads

4 comments:

Anonymous said...

Great !!

Nikunj Dhawan

Stephen J. Naughton said...

Thanks Nikunj, your comment is much appreciated :)

Steve

Anonymous said...

Great Sample! I really appreciate your posts about dynamic data. you I have one question, is it possible to use this approach to add dynamic hyper links to GridView? for example I want to be able to add some links for some of my entities. for example for Contact Entity I like to add order_sevice.aspx?id= link in GridView which is a non-dynamicdata page.

Stephen J. Naughton said...

You should be abe to link to any static (none routed page) no problem my sample allows you to change the page action for a particular table.

Steve