Introduction
The Page Template and Scaffolding features provided by the ASP.NET Dynamic Data Framework can save us serious amounts of time and effort by potentially limiting general customization work to only four or five page templates, regardless of how many entities there are in our data models. The Dynamic Data Framework's default routing mode, known as separate-page mode, in a new Dynamic Data site is to have one page each for the List, Details (view a record read-only), Edit, and Insert actions. The other pre-packaged routing mode, or combined-page mode, uses a single page for List, Details, Edit, and Insert actions. The combined-page mode site model simply uses a grid and a details panel on the same page, while the former has a list, grid, or page from where we can navigate to the edit or insert pages. This article focuses on how to use a single, detail view oriented, Page Template for edit, view, and insert actions.
Routing
Separate-page Mode
Both routing modes mentioned above are included as part of the boilerplate code for Global.asax whenever you create a new Dynamic Data site in Visual Studio. The separate-page mode is active by default:
routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
Model = model
});
Listing 1 - Routing definition for Separate-page mode.
True to recently established customs, the comments are slightly confusing, as they instruct you to "uncomment the following route definition" when it is actually already uncommented. I suppose this may help if you, at some point, comment the route definition out and forget how things work at a later stage.
This routing definition allows four values for the action substitution parameter, and doesn't specify any view. This means that each action will be routed to its own view. For example, a request with a URL of Customers/List.aspx will be routed to the List.aspx page, which will display a list of records from the Customers table, while a URL of Customers/Insert.aspx will cause a request to be routed to the Insert.aspx page, to insert a new Customer record.
One of the nice things about ASP.NET routing is that it allows us to map 'neat' request URLs, i.e., without file extensions, to real views with file extensions, so we could define a route where the URL need only be Customers/List (no ".aspx") to still be routed to the List.aspx page. I have absolutely no idea why Microsoft has not done this in the Dynamic Data Framework, but will investigate in a future article.
Combined- page mode is easy to activate, but in this case, the comments are true; the code really has to be uncommented. I have done so for the sake of clarity, but for the purposes of this article, and our focus on the separate-page mode, it should actually remain commented out.
Combined-page Mode
Listing 2 - Combined-page mode routing definition
These route definitions cause all requests to be routed to the ListDetails.aspx page, which displays a list and edit panel at the same time on the same page. This mode is very clumsy, and without extensive customization of ListDetails.aspx, is only really suitable for those situations where you need a maintenance UI.
If we look at the four page templates used by the Separate-page mode, we can see that Details.aspx, Edit.aspx, and Insert.aspx are almost identical. Details.aspx, which displays read-only data, even has exactly the same ValidationSummary
and DynamicValidator
controls as Edit.aspx!
Figure 1 - Mysterious validators in a read-only page
It seems that even the Dynamic Data development team sometimes engages in some 'copy and paste coding'.
If our project doesn't require extensive customization to these page templates, why not combine them into one page? Even if maintaining only four pages means negligible work, concentrating all the layout and functionality into one page reduces chances of errors creeping in, such as the copy and paste issue I mentioned above. Without giving too much thought to a name for this new page template, let's call it DetailsEditInsert.aspx and move on.
Implementing our New Page Template
I assume that most readers of this article will know how to create a new Dynamic Data site, add a data model to it, and register the data model in Global.asax. If you would like an introduction to Dynamic Data, or a walkthrough of getting a new site up and running, please refer to the Microsoft Dynamic Data website, or do a search and find many great articles covering the very basics.
I'll begin by creating a new Dynamic Data website that uses a data model and the ubiquitous Northwind database. It's not at all very important that your project is identical to the one I use here, or in the example code. As long as you have some scaffolded tables in a working Dynamic Data site, you will be able to follow me. That is one of the beautiful things about the Dynamic Data Framework and its templating.
Adding the New Page
I've created a new solution called DynamicDataArticles, and added a new website called AllActions to it.
Let's create the New Page template in the Dynamic Data\PageTemplates folder:
Figure 2 - The New Page template
The Others
Now, let's take a closer look at the other detail pages: Details.aspx, Edit.aspx, and Insert.aspx. As I noted before, they are all strikingly similar. All of these share a basic structure and set of key controls:
- A
DynamicDataManager
above an UpdatePanel
that contains all the other dynamic data and standard controls. The DynamicDataManager
may not be located inside an UpdatePanel
.
- A
DetailsView
and an EntityDataSource
control. Our interest lies with the DetailsView
.
- The
UpdatePanel
and ScriptManageProxy
controls used for AJAX functions.
A fairly informative starting point is to copy all the layout and code from Details.aspx into DetailsEditInsert.aspx. Of course, you can always just create a copy of Details.aspx and rename it. Let's tell Dynamic Data to route all detail type (non-list) requests to our new page. Carefully change the single, default route definition in Global.asax, as follows:
routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
Constraints = new RouteValueDictionary(
new { action = "List|Details|Edit|Insert" }),
Model = model
});
Listing 1 - Before first route changes
routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
Constraints = new RouteValueDictionary(new { action = "Details|Edit|Insert" }),
ViewName = "DetailsEditInsert",
Model = model
});
Listing 2 - After first route changes
See how we've narrowed the actions allowed in this route definition down to only the three detail type actions. This is because we are now specifying a view name, meaning all actions for this definition will get routed to the DetailsEditInsert.aspx view, whereas before, each action was routed to its corresponding view. It's simply not appropriate to also route the List action to DetailsEditInsert.aspx, so we move that action out to add a new, separate route definition only for list actions:
routes.Add(new DynamicDataRoute("{table}/List.aspx")
{
ViewName = "List",
Action = PageAction.List,
Model = model
});
Listing 3 - New route definition for the "List" action
Note how I no longer use a substitution parameter for the action, because there is only one possible action for this route definition. Because I don't use an action parameter, I need to tell Dynamic Data what my action is, hence the PageAction.List
property.
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies)
could have been removed, had its name changed, or is temporarily unavailable.
Please review the following URL and make sure that it is spelled correctly.
Requested URL: /AllActions/DynamicData/PageTemplates/List.aspx
This happens because Visual Studio loses track of its Start Page for this site, and although the requested resource (/PageTemplates/List.aspx) does in fact exist, in Dynamic Data, we have to be routed to that resource. We cannot navigate there directly, as the DynamicDataManager
control on the page will not have enough metadata to properly initialize the page. We can easily fix this by setting Default.aspx as the Start Page for this web site in Visual Studio.
Let's view the site in the browser and see if our new page is receiving requests. If we browse to Default.aspx, we should see a list of tables from our data model. Hover over one of the table links, and note the URL for the link. It should be ~/{table}/List.aspx, and clicking it should, of course, execute the List action for that table and show us a grid with the contents of the table.
Figure 4 - List view of the Orders table
Take a look at the last part of the URLs for the Edit, Details, and Insert new item links. They all specify the action corresponding to the link. However, if we click on any of these links, our modified route definitions will ensure that we always end up on the DetailsEditInsert.aspx page. Things get more interesting when we click the Insert new item link:
Figure 5 - First attempt to Insert
Two details stand out here. The really major one is that the form is already populated! The other detail is actually very minor, being the heading says "Entry from table Orders", when it should say, "Add new entry to table Orders". These are both clues that our triple-purpose DetailsEditInsert.aspx doesn't know that it's supposed to be doing an insert. We copied the functionality of the Details.aspx verbatim into our new page, so we can't blame it for being confused. Note the value of the OrderID field. As our new page is trying to display details for an Order, but has no primary key, it settles on the first record in the Orders table, which has an OrderID of 10248. You can confirm this by viewing the data in the Orders table using, e.g., Server Explorer. Let's take care of the major details first.
As much as I would have liked to have routing details available to DetailsEditInsert.aspx so that we can see which action routed to this page, I could find no way of achieving this. I settled on examining the request URL:
protected void Page_Init(object sender, EventArgs e)
{
dynamicDataManager.RegisterControl(commonDetailsView);
switch (Request.Url.Segments[3])
{
case "Insert.aspx":
commonDetailsView.AutoGenerateInsertButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Insert);
break;
}
}
Listing 4 - Giving the multipurpose page a single purpose
An Aside on Object Naming
Two control names in the above code will most likely look strange to you. Whenever I create a new page template in Dynamic Data, or when I customise the default templates, I revise the object naming in these templates to reflect my own standards and conventions. I recommend you do this as well, as the nice thing with Dynamic Data is that you seldom need to do this. Instead of doing it for every project, do it once for each standard page template, and simply replace the default templates with your own every time you create a new project.
I have renamed DynamicDataManager1
to dynamicDataManager
, DetailsView1
to commonDetailsView
, and DetailsDataSource
to detailsDataSource
. The "1" suffix always strikes me as making code or mark-up look sloppy and unfinished. I have also renamed other controls not germane to this article, and the complete result of my naming revision can be seen in the DetailsEditInsert.aspx page and its code-behind in the sample project.
My method in the above code is not the most elegant, nor robust, but for most purposes, it is quite adequate. The URLs are supplied by the Dynamic Data framework, and should always have at least four segments, the last of which should always be Details.aspx, Edit.aspx, Insert.aspx, or List.aspx, so I feel comfortable hard-coding the segment index as well as the strings in non-production web sites.
Comparing the original Insert.aspx to our new page also reveals some other key differences. First, the ItemInserted
and ItemCommand
events of commonDetailsView
are not handled. All that both of these do is to return to the list view; the ItemCommand
handler does this when the Cancel link is clicked, and the ItemInserted
handler does the same, on condition that no errors occurred during the insert. The latter is worth mentioning because Dynamic Data will just leave you staring at the Insert view if any exceptions occur during the insert operation, with no indication of why nothing is happening!
Second, detailsDataSource
needs to allow inserts as well. By default, on our new page (and the Details.aspx page), only deletes are allowed.
<asp:EntityDataSource ID="detailsDataSource"
runat="server" EnableDelete="true"
EnableInsert="true" EnableUpdate="true">
<WhereParameters>
<asp:DynamicQueryStringParameter />
</WhereParameters>
</asp:EntityDataSource>
Listing 5 - Enabling Inserts on the data source
Readers are well advised to add a control to the Insert.aspx and Edit.aspx page templates for displaying an error message, and to modify the ItemInserted
and ItemUpdated
event handlers, respectively, to detect exceptions and display an error message. Of course, those of us using DetailsEditInsert.aspx need only modify a single page (with both event handlers).
After making the above code changes, i.e., adding the event handlers and enabling inserts, our insert action using DetailsEditInsert.aspx works as expected, inserting the new item and returning us to the List view. Let's move on to getting the Edit action working as well.
Figure 7 - First attempt at editing
As we can see in the above screenshot of the DetailsEditInsert.aspx form we see after clicking the Edit link on any item in the List view, our multi-purpose page template doesn't know it is supposed to be in Edit mode. Its rendered UI looks exactly like that of the original Details.aspx page, complete with an Edit link that would normally take you to the Edit view. In this case, it simply reloads DetailsEditInsert.aspx, complete with an Edit link.
Let's repeat the process we followed a short while ago to get the Insert action working. First, we need to tell the multi-purpose page what its current purpose is:
protected void Page_Init(object sender, EventArgs e)
{
dynamicDataManager.RegisterControl(commonDetailsView);
switch (Request.Url.Segments[3])
{
case "Insert.aspx":
commonDetailsView.AutoGenerateInsertButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Insert);
break;
case "Edit.aspx":
commonDetailsView.AutoGenerateEditButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Edit);
break;
}
}
Listing 6 - Adding the 'Edit Purpose' to our page
This is the most important change to allow us to execute the Edit action with our multi-purpose page. Then, we must also enable edits on the entityDataSource
control, i.e., add the EnableUpdate="true"
attribute to its declaration in the mark-up, and add a handler for the ItemUpdated
event of commonDetailsView
.
Last Finesses
I hope most of you couldn't help feeling a little uneasy about the extra Edit and Delete links on our multipurpose page, in any of the three modes we implemented for that page template.
Figure 8 - Unexplained links
It's also not apparent in the above image which mode the page is in, because the same heading, "Entry from table Region", always appears (only for all Region table actions, of course), regardless of the page's mode. Let's fix these two issues before considering our work here over and hitting the pub.
Why are those Edit and Delete links being generated by our multipurpose template? Looking at the mark-up for DetailsEditInsert.aspx, we can see that commonDetailsView
has a single template control declared in its Fields
collection, and this template control defines the two problem links. It's simple enough to just delete the whole Fields
element from the mark-up, but why are these links defined in the first place? Remember, we based our multipurpose page on the Details.aspx page, so that page probably needed these links; ergo they are only needed when our new page is in Details mode.
Adding and removing this template control field when we set our mode seems like a bit much work, and it's work I wasn't really interested in when I devised this project. Some experimentation quickly revealed that we can order a DetailsView
control to automatically generate these links just as we do with the Update and Insert links. Let's add a third case to our mode switch:
switch (Request.Url.Segments[3])
{
case "Insert.aspx":
commonDetailsView.AutoGenerateInsertButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Insert);
break;
case "Edit.aspx":
commonDetailsView.AutoGenerateEditButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.Edit);
break;
case "Details.aspx":
commonDetailsView.AutoGenerateEditButton = true;
commonDetailsView.AutoGenerateDeleteButton = true;
commonDetailsView.ChangeMode(DetailsViewMode.ReadOnly);
break;
}
Listing 7 - Final version of the mode switch
Now, if we click on a Details link in the List view, we get nearly everything we need, plus the two extra, problem links:
Figure 9 - Our own Edit and Delete links
If we now simply remove the explicit links from the Fields
collection of commonDetailsView
, what do we have to lose? Only one thing really, and that is the JavaScript delete confirmation that is attached to the client Click
event of the Delete link, as seen in the LinkButton
marked up for the Delete link, in "OnClientClick
":
<asp:LinkButton ID="DeleteLinkButton"
runat="server" CommandName="Delete" CausesValidation="false"
OnClientClick='return confirm("Are you sure you want to delete this item?");'
Text="Delete" />
Listing 9 - JavaScript delete confirmation
If you click the bottom-most Delete link, as seen in the image above, you will get no confirmation. The displayed record will be deleted, and you will be routed back to the List view without passing Begin. Now, once we delete the two 'problem' links (notice that now 'problem' is in quotes), how can we regain the fairly useful delete confirmation? Well, seeing as the Delete link is automatically generated, we can be fairly confident that its text will always be "Delete". Armed with this assumption, a quick and dirty jQuery solution is not far away.
<script src="http://localhost:28537/AllActions/Scripts/jquery-1.3.2.js"
type="text/javascript"></script>
<script type="text/javascript">
jQuery(function($) {
$(".detailstable a:contains('Delete')").click(function() {
return confirm("Are you sure you want to delete this item?");
});
});
</script>
Listing 10 - jQuery delete confirmation
Notice how I've had to use an absolute URL for the jQuery script reference. This is because of the 'artificial' URLs used by Dynamic Data routing. If I had used a relative URL, such as Scripts/jQuery-1.3.2.js, when the browser requests a page with a URL ending in, e.g., Categories/List.aspx, it also effectively requests the non-existent resource Categories/Scripts/jQuery-1.3.2.js, and gets a 404 - Not found response. This caused me some consternation, as I could see that jQuery was loaded in Default.aspx, and could not figure out why it wasn't loaded in Categories/List.aspx. Using advanced debugging tools such as Firebug, this took all of about ten minutes to resolve.
In a nutshell, the jQuery function above executes when the document element is ready in the browser DOM. It looks for all "a
" elements (links) that contain the text "Delete", that are descendents of any elements with a CSS class of "detailstable
", and attaches an anonymous event handler to the "click
" event of these link elements, which does exactly the same as the one defined above for the LinkButton
. The mark-up for the DetailsEditInsert.aspx page tells us that commonDetailsView
has the CSS class "detailstable
", and because the Delete link is automatically generated for us, we should have some confidence that the link will always contain that text.
Readers not familiar with jQuery could do a lot worse than to download it, add it to a simple HTML only project, open up the jQuery API Browser, and spend a few hours learning the basics. This API and library is definitely one where fairly advanced skills quickly bootstrap themselves from very elementary basics. The only ASP.NET alternatives for implementing our delete confirmation that I can imagine seem not just clumsy, but positively handicapped, in comparison to the above two-liner, but then maybe an astute reader, with a better imagination than me, can offer a pure ASP.NET solution.
Epilogue
Let's take another look at what we've accomplished. We have added a new page template called DetailsEditInsert.aspx that performs all the functions of the Details.aspx, Edit.aspx, and Insert.aspx page templates provided "out of the box" by the Dynamic Data project templates. We have learned a little about Dynamic Data routing, and I hope some of that may rub off on our knowledge of routing in general (mine was nearly non-existent before embarking on this project, and we've used a minimal example of the power of JavaScript, harnessed through jQuery).
This is my first article, in what I see becoming quite a collection, on the shiny and juicy goodies available in ASP.NET Dynamic Data. As I publish future articles, I will revise sections in this one that need exploration in much greater detail to link to the new ones, and I will continue to revise the growing collection to try and maintain a useful web of articles for those interested in Dynamic Data. Please stay tuned, and assist me where possible with suggestions, correction, and criticism.