Tuesday 1 July 2008

Dynamic Data and Routes (Take 2)

Basic Routes

Most of what I writing about comes from this thread Manual Scaffolding and Routing on the DynamicData forum ASP.NET Dynamic Data.

The basic route takes the form:

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

Listing 1

or

routes.Add(new DynamicDataRoute("{table}.aspx")
{
    Action = PageAction.List,
    Model = model
});

Listing 2

In Listing 1 the first of the two routes above will handle any of the four actions "List|Details|Edit|Insert"  for any table

http://localhost:3759/DD_NWDBP/Orders/List.aspx

In the above Orders equates to {table} and List equates to {action}

In Listing 2 the second of the two routes, there is no {action} defined but only an Action and will only handle the "List" action, and will have a path similar to:

http://localhost:3759/DD_NWDBP/Orders.aspx

Orders equates to {table}

So the difference between Action and Constraints is:

David Ebbo said here: The quick summary is:

  • Setting Action sets the action to be used when the path doesn't contain {action}.  e.g. see the ListDetails.aspx routes in global.asax (commented out by default)

  • Setting Constraints limit the set of acceptable value for a variable like {action}.  So unlike Action, it only makes sense to have it when you do have {action} in the path.

Routes with Parameters

So lets add a route with a parameter:

routes.Add(new DynamicDataRoute("Orders/{Id}/{action}.aspx")
{
    Constraints = new RouteValueDictionary(new { action = "Details|Edit" }),
    Model = model
});

Listing 3

Here we have {Id} parameter for the Orders table this has a URL like:

http://localhost:3759/DD_NWDBP/Orders/10248/Details.aspx
Where 10248 is the primary key of the Order we see on the details page. What's really neat is that we can also dispense with the .aspx at the end making it:
routes.Add(new DynamicDataRoute("{table}/{Id}/{action}")
{
    Constraints = new RouteValueDictionary(new { action = "Details|Edit" }),
    Model = model
});

Which give the URL that common look that we see all the time now:

http://localhost:3759/DD_NWDBP/Orders/10248/Details

Now we can move things about a bit.

routes.Add(new DynamicDataRoute("{table}/{action}/{Id}")
{
    Constraints = new RouteValueDictionary(new { action = "Details|Edit" }),
    Model = model
});
The URL :
http://localhost:3759/DD_NWDBP/Orders/Details/10248

You could also move the other elements of the URL about and it will still work {Id}/{table}/{action}, the order you put these URL parameters is up to you, what ever works for your application.

Referring back to the ASP.NET Dynamic Data forum thread Manual Scaffolding and Routing Marcin proposed this solution the issue of having to create a route manually for each table.

foreach (MetaTable table in model.Tables)
{
    if (table.PrimaryKeyColumns.Count == 1)
    {
        string pkName = table.PrimaryKeyColumns[0].Name;
        string routeUrl = String.Format("{0}/Edit/{{{1}}}.aspx", table.Name, pkName);
        routes.Add(new DynamicDataRoute(routeUrl)
        {
            Model = model,
            Table = table.Name,
            Action = PageAction.Edit
        });
    }
}
Note: that Marcin uses Action = PageAction.Edit instead of Constraints this is because there is no {action} match in the string

This creates a series of routes of the form {Orders}/Edit/{OrderID}.aspx

This got me thinking what about Details the original question in the thread related to "a more Search engine friendly Url" so I posted:

foreach (MetaTable table in model.Tables)
{
    if (table.PrimaryKeyColumns.Count == 1)
    {
        string pkName = table.PrimaryKeyColumns[0].Name;
        string routeUrl = String.Format("{0}/{{action}}/{{{1}}}", table.Name, pkName);
        routes.Add(new DynamicDataRoute(routeUrl)
        {
            Model = model,
            Table = table.Name,
            Constraints = new RouteValueDictionary(new { action = "Details|Edit" }),
        });
    }
}

routes.Add(new DynamicDataRoute("{table}/{action}")
{
    Constraints = new RouteValueDictionary(new { action = "List|Insert" }),
    Model = model
});
Note: That I am using the Constraints instead of the Action this is because I have added an {action} match to the string, this allows the action to be matched to any action but constrained by the Constraints: 
Constraints = new RouteValueDictionary(new { action = "List|Insert" }).

Which as you can see makes the Action cover both Edit and Details but this only works with table that have a single column as primary key, what about compound primary keys with two or more columns? I came up with this:

// adding embedded PK parameters to routing
foreach (MetaTable table in model.Tables)
{
    string routeUrl = "";
    switch (table.PrimaryKeyColumns.Count)
    {
        case 1:
            string pkName = table.PrimaryKeyColumns[0].Name;
            routeUrl = String.Format("{0}/{{action}}/{{{1}}}", table.Name, pkName);
            break;
        case 2:
            string pkName1 = table.PrimaryKeyColumns[0].Name;
            string pkName2 = table.PrimaryKeyColumns[1].Name;
            routeUrl = String.Format("{0}/{{action}}/{{{1}}}/{{{2}}}", table.Name, pkName1, pkName2);
            break;
// adding more case statements here would allow many PK's } routes.Add(new DynamicDataRoute(routeUrl) { Model = model, Table = table.Name, Constraints = new RouteValueDictionary(new { action = "Details|Edit" }) }); } routes.Add(new DynamicDataRoute("{table}/{action}") { Constraints = new RouteValueDictionary(new { action = "List|Insert" }), Model = model });

And this again got me thinking; this will work but what if you have lots of tables then this will create lots of routes. This gave me the Idea what if you knew that a lot of your table have the same name for the primary key, say Id. so I came up with this:

// adding embedded PK parameters to routing
// add default route with PKname = Id
routes.Add(new DynamicDataRoute("{table}/{action}/{Id}")
{
    Constraints = new RouteValueDictionary(new { action = "Edit|Details"}),
    Model = model
});

// add a route for each unique table PK combination NOT PKname = Id
foreach (MetaTable table in model.Tables)
{

    if (table.PrimaryKeyColumns.Count == 1 && table.PrimaryKeyColumns[0].Name == "Id")
        continue; // if one PK and name = Id continue
    string routeUrl = "";
    switch (table.PrimaryKeyColumns.Count)
    {
        case 1:
            string pkName = table.PrimaryKeyColumns[0].Name;
            routeUrl = String.Format("{0}/{{action}}/{{{1}}}", table.Name, pkName);
            break;
        case 2:
            string pkName1 = table.PrimaryKeyColumns[0].Name;
            string pkName2 = table.PrimaryKeyColumns[1].Name;
            routeUrl = String.Format("{0}/{{action}}/{{{1}}}/{{{2}}}", table.Name, pkName1, pkName2);
            break;
    }

    routes.Add(new DynamicDataRoute(routeUrl)
    {
        Model = model,
        Table = table.Name,
        Constraints = new RouteValueDictionary(new { action = "Edit|Details"})
    });
}

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

The first step is to create your common Routes and then when iterating through the list of tables in the MetaModel you skip out if the any table matches your common route.

I'm posting on this so it's where I can find it easily again and also because I think Routing is really neat. smile_teeth

Note: David Ebbo posted a great example of a custom route class here have a look it's a better solution than the above for embedding PK's into URL's

1 comment:

Caio said...

Hello,great article,but what if I would want to hide my IDs from URL?
Can you suggest me a way to do it?
Thank you