Using User Controls as Page Templates in Dynamic Data

Dynamic Data has the concept of Page Templates, which are pages that live under ~/DynamicData/PageTemplates and are used by default for all tables.  Recently a user on the forum asked whether they could use User Controls instead of Pages for those templates.

To me, this means potentially two distinct scenarios, and I tried to address both in this post:

  1. Using routing: in this scenario, you still want all your requests to go through the routing engine, but have the user control templates somehow get used.  The URLs here would still  look identical to what they are in a default Dynamic Data app, e.g.  /app/Products/List.aspx (or whatever you set them to be in your routes).
  2. No routing: in this scenario, you want the URL to go directly to a specific .aspx page, and then have that page do the right thing to use Dynamic Data through the User Control templates.

Creating the User Controls

First, let’s look at what will be the same for both cases.  Under ~/DynamicData/PageTemplates, I deleted all the aspx files (to prove that they’re not used), and instead created matching ascx files (i.e. User Controls).  Those user controls contain essentially the same things that were in the pages, minus all the ‘outer’ stuff (e.g. things that relate to the master page).

Making it work using routing

Now let’s look specifically at case #1 above, where we want to use routing.  First, we need to make a small change to the route to use a custom route handler:

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

Note how we set RouteHandler to our own custom handler type.  Now let see what this type looks like:

 public class CustomDynamicDataRouteHandler : DynamicDataRouteHandler {
    public override IHttpHandler CreateHandler(DynamicDataRoute route, MetaTable table, string action) {
        // Always instantiate the same page.  The page itself has the logic to load the right user control
        return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath("~/RoutedTestPage.aspx", typeof(Page));
    }
}

It’s basically a trivial handler which always instantiates the same page!  That may look strange, but the idea is that all the relevant information is carried by the route data, which that page can then make use of.  Now let’s see what we’re doing in this one page.  It’s itself pretty trivial, with just a bit of logic in its Page_Init:

 protected void Page_Init(object sender, EventArgs e) {
    // Get table and action from the route data
    MetaTable table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
    var requestContext = DynamicDataRouteHandler.GetRequestContext(Context);
    string action = requestContext.RouteData.GetRequiredString("action");

    // Load the proper user control for the table/action
    string ucVirtualPath = table.GetScaffoldPageVirtualPath(action);
    ph.Controls.Add(LoadControl(ucVirtualPath));
}

The first 3 lines show you what it takes to retrieve the MetaTable and action from the route date.  Then the next couple lines load the right user control for the situation.  GetScaffoldPageVirtualPath is a simple helper method which has logic to locate the proper ascx:

 public static string GetScaffoldPageVirtualPath(this MetaTable table, string viewName) {
    string pathPattern = "{0}PageTemplates/{1}.ascx";
    return String.Format(pathPattern, table.Model.DynamicDataFolderVirtualPath, viewName);
}

And that’s basically it for the routed case!

Making it work without routing

Now let’s look at the non-routed case.  Here, the routes are not involved, so the URL goes directly to a page.  That page is similar to the one above, but with some key differences.  Here is what it does in it Page_Init:

 protected void Page_Init(object sender, EventArgs e) {
    // Get table and action from query string
    string tableName = Request.QueryString["table"];
    string action = Request.QueryString["action"];
    
    // Get the MetaTable and set it in the dynamic data route handler
    MetaTable table = MetaModel.Default.GetTable(tableName);
    DynamicDataRouteHandler.SetRequestMetaTable(Context, table);

    // Load the proper user control for the table/action
    string ucVirtualPath = table.GetScaffoldPageVirtualPath(action);
    ph.Controls.Add(LoadControl(ucVirtualPath));
}

The key difference  is that it can’t rely on route data being available, so it needs to get the table and action information from  somewhere else.  You can come up with arbitrary logic for this, but the most obvious way is to just use the query string (e.g. ?table=Products&action=List).

Then  it does sort of the reverse of the case above, and sets the MetaTable into the route handler.  Even though routing is not used, Dynamic Data tries to get the MetaTable from DynamicDataRouteHandler, so having it there allows many things to just work.

And finally, it loads the user control using the exact same steps as above.  And that’s that!

The code is attached below.

DynamicDataWithUserControls.zip