In This Chapter
• Creating a New Page Template
Implementation of CRUD (Create, Read, Update and Delete) operations has been an ongoing challenge in web applications. The problem is not so much their complexity, but rather a sheer number of the pages that have to be implemented in a typical enterprise application. It is common in web applications to have a separate web page for implementing each type of operation, with one page for editing a particular entity type and another for displaying it in Read-only mode. The resulting number of pages can quickly become unmanageable in enterprise applications that have dozens and sometimes hundreds of entity types.
In traditional ASP.NET WebForms applications, developers sometimes try to address this problem by creating multi-purpose pages, such as a single page that allows users to both create and modify instances of a particular entity type. Visual Studio even offers limited support for creating multi-purpose pages by generating the Item, Edit, and Insert templates for FormView
controls. Although this approach reduces the overall number of pages in the application, it does so at the cost of significantly increasing complexity of the individual pages.
The ASP.NET MVC framework attempts to solve this problem by offering scaffolding functionality in Visual Studio, which generates separate CRUD pages for a given entity at design time. Although design-time scaffolding helps create the initial version of the CRUD pages, the task of maintaining the generated code falls back onto developers’ shoulders. In a way, the design-time scaffolding solves the problem of supporting CRUD operations by creating a bigger problem of rapidly increasing the number of pages and amount of code developers have to maintain.
ASP.NET Dynamic Data offers an alternative solution to the CRUD problem. Instead of generating pages at design time, like the WebForms and MVC tools do, it generates pages at runtime with the help of page templates.
When users of a web application need to perform a particular action, such as creating, reading, or modifying an instance of a particular entity type, Dynamic Data can use a single dynamic page template to handle this action across all entity types:
namespace System.Web.DynamicData
{
public static class PageAction
{
public static string Details { get { return "Details"; } }
public static string Edit { get { return "Edit"; } }
public static string Insert { get { return "Insert"; } }
public static string List { get { return "List"; } }
}
}
Out of the box, Dynamic Data understands actions defined in the PageAction
class and provides page templates (see Figure 6.1) for inserting, displaying details, editing, and listing entities as part of the new website and web application projects in Visual Studio.
Page templates are special ASPX forms located in the DynamicData/PageTemplates
folder of the web application. They rely on URL routing functionality of ASP.NET to handle page requests for different entity types. Dynamic Data applications register at least one DynamicDataRoute
during the application startup. Here is an example of the route registration code you normally find in the Global.asax
:
RouteTable.Routes.Add(new DynamicDataRoute("{table}/{action}.aspx"));
The static Routes
property of the RouteTable
class provides access to the global collection of Route
objects. This code uses it to register a new DynamicDataRoute
that defines a URL as a combination of a table name and a page action. The {table}
marker defines the place where you expect to find a table name in the URL, and the {action}
marks the place where you expect to find the page action. If an incoming request has a URL in this format, the default MetaModel
of the web application contains a table with the matching name, and a page template exists matching the action, Dynamic Data uses this page template to handle the request. In the following URL, Products matches the table parameter, and List matches the action:
http://localhost/Products/List.aspx
The Details page template generates web pages that display a single entity instance in a form view. Figure 6.2 shows an example of such page generated for the Product entity. Notice the URL in the address bar of the web browser; it includes the name of the table (Products) and a query string that specifies value of the entity’s primary key (ProductID). The Details page template uses this information to retrieve the appropriate entity instance from the database.
Listing 6.1 shows the source code of the Details page template, Details.aspx
, located in the DynamicDataPageTemplates
folder of the web application. Most of its contents should be already familiar to you from Chapter 2, “Entity Framework,” and Chapter 4, “Entity Templates.”
<%@ Page Language="C#"
MasterPageFile="~/Site.master" CodeBehind="Details.aspx.cs"
Inherits="WebApplication.DynamicData.PageTemplates.Details" %>
<asp:Content ContentPlaceHolderID="main" runat="Server">
<asp:DynamicDataManager runat="server">
<DataControls>
<asp:DataControlReference ControlID="formView" />
</DataControls>
</asp:DynamicDataManager>
<h2>Entry from table <%= table.DisplayName %></h2>
<asp:UpdatePanel runat="server">
<ContentTemplate>
<asp:ValidationSummary runat="server" HeaderText="List of validation errors"/>
<asp:DynamicValidator runat="server" ControlToValidate="formView"
Display="None" />
<asp:FormView runat="server" ID="formView" DataSourceID="dataSource"
OnItemDeleted="FormView_ItemDeleted">
<ItemTemplate>
<asp:DynamicEntity runat="server" />
<div class="DDFormActions">
<asp:DynamicHyperLink runat="server" Action="Edit" Text="Edit" />
<asp:LinkButton runat="server" CommandName="Delete" Text="Delete"
OnClientClick='return
confirm("Are you sure you want to delete this item?");'/>
<asp:DynamicHyperLink runat="server" Action="List" Text="Back to List"/>
</div>
</ItemTemplate>
</asp:FormView>
<asp:EntityDataSource ID="dataSource" runat="server" EnableDelete="true" />
<asp:QueryExtender TargetControlID="dataSource" runat="server">
<asp:DynamicRouteExpression />
</asp:QueryExtender>
</ContentTemplate>
</asp:UpdatePanel>
</asp:Content>
The EntityDataSource
control is used to retrieve the required entity instance from the database. However, as you probably noticed, it does not specify the DefaultContainerName
and the EntitySetName
properties normally required for the control to know which ObjectContext
to instantiate and which object set to query. This information is supplied to the EntityDataSource
by the DynamicDataManager
, one of the new controls provided by Dynamic Data.
The DynamicDataManager
control has a DataControls
collection that stores one or more DataControlReference
objects. In the Details page template, it stores a single reference to the FormView
control. During the initialization phase of the page lifecycle, the DynamicDataManager
uses this reference to find the EntityDataSource
control associated with the FormView
and configures it. In particular, it extracts the table name from the current URL route and obtains the MetaTable
object describing the table. The MetaTable
object provides the information necessary to initialize the EntityDataSource
, including the type of the ObjectContext
as well as the name of the entity set to query.
Knowing the context type and the entity set is not enough to retrieve a specific entity from the database. The query sent to the database server must also include a WHERE
clause that identifies the table row where it is stored. This task is performed by the QueryExtender
control, which is associated with the EntityDataSource
. In the Details page template, the QueryExtender
uses a special type of data source expression, called DynamicRouteExpression
. At runtime, this object extracts the values of the primary key columns from the URL and modifies the query generated by the EntityDataSource
to include appropriate filter criteria.
The FormView
control defines an ItemTemplate
to display the entity with the help of the DynamicEntity
control. As you remember, this control uses Dynamic Data entity templates to generate either a generic, single column view of the entity or a custom view if a custom template was defined for the entity. In the Details page template, the DynamicEntity
control does not specify the Mode
property, so a read-only entity template is used.
In addition to the DynamicEntity
, the ItemTemplate
of the FormView
control also includes a set of links for users to edit or delete the entity or to go back to the List page. The Edit and List actions are implemented by the DynamicHyperLink
controls. When databound to an entity instance, a DynamicHyperLink
control generates a URL that points to another dynamic page based on the value of the control’s Action
property. In this example, the URL generated by the first DynamicHyperLink
control looks like this:
http://localhost:49212/Products/Edit.aspx?ProductID=1
In this URL, Products
is the name of the MetaTable
describing the current entity instance, Edit
is the name of the action specified in the Action
property of the DynamicHyperLink
control, and ProductID=1
is the name and value of the primary key column of the current entity instance. Because the second DynamicHyperLink
’s Action
property set to List
, the URL it generates looks like this instead:
http://localhost:49212/Products/List.aspx?ProductID=1
The Delete action, on the other hand, is implemented by a traditional LinkButton
. This control specifies the CommandName
property and takes advantage of the deletion support built into the FormView
. When the user clicks the link generated by this control, a Delete command is submitted back to the FormView
, which in turn passes the deletion request to the data source control associated with it. To allow deletion, the EntityDataSource
control has its EnableDelete
property set to true.
Not all of the functionality is implemented directly in the markup of the Details page template. You might have noticed that the caption line relies on the table
field defined in the code-behind.
<h2>Entry from table <%= table.DisplayName %></h2>
Additional code is required to initialize controls and handle entity deletion. Listing 6.2 shows the code-behind of the Details page template.
using System;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
namespace WebApplication.DynamicData.PageTemplates
{
public partial class Details : System.Web.UI.Page
{
protected MetaTable table;
protected void Page_Init(object sender, EventArgs e)
{
this.table = DynamicDataRouteHandler.GetRequestMetaTable(this.Context);
this.Title = this.table.DisplayName;
this.dataSource.EntityTypeFilter = this.table.EntityType.Name;
this.dataSource.Include = this.table.ForeignKeyColumnsNames;
}
protected void FormView_ItemDeleted(object sender, FormViewDeletedEventArgs e)
{
if (e.Exception == null ¦¦ e.ExceptionHandled)
this.Response.Redirect(this.table.ListActionPath);
}
}
}
The Page_Init
event handler initializes the table
field by extracting the table name from the URL route and looking up the matching MetaTable
object in the default MetaModel
. DisplayName
of the table is used to initialize the Title
of the page, but more importantly, this code performs the missing initialization steps required for the EntityDataSource
.
The EntityTypeFilter
property of the EntityDataSource
specifies the name of the entity type, which might be different from the entity set if your data model uses entity inheritance. The Northwind sample data model doesn’t take advantage of this feature, but imagine that a Contact table was used to store both Customer and Employee information with a ContactType column serving as the discriminator. The same entity set, Contacts, could be used to retrieve both Customer and Employee entity type, and you would need to specify the EntityTypeFilter
to make sure a Customer entity is not displayed as if it were an Employee. The Page_Init
handler sets the EntityTypeFilter
property using the entity type name retrieved from the MetaTable
object.
The Include
property of the EntityDataSource
determines whether related entities will be retrieved along with the main one. In the case of the Product entity, Category and Supplier entities also need to be retrieved from the database to display the category and supplier names in human-readable form, as opposed to the raw numeric values of the CategoryID and SupplierID columns. The Page_Init
handler sets the Include property using the names of the foreign key navigation properties of the entity type, retrieved from the MetaTable
object.
The FormView_ItemDeleted
event handler is invoked when the user clicks the Delete LinkButton
and the current entity instance has been successfully deleted. Instead of displaying an empty page, this method redirects the user back to the List page that displays all remaining entities of that type.
The Edit page template generates a web page for editing a single entity instance. Figure 6.3 shows such a page generated for a Product entity.
This page is quite similar to the Details page discussed earlier. The URL also includes table and action names as well as the names and values of the primary key columns of the current entity instance.
http://localhost:49212/Products/Edit.aspx?ProductID=1
Users can navigate to this page by clicking the Edit link on the pages generated by the Details page template, as well as on the pages produced by the List page template, which is discussed shortly.
As you would expect, implementation of the Edit page template is similar to the Details page template. Listing 6.3 shows the markup file of this template, with significant differences between the templates indicated by bold font.
<%@ Page Language="C#" MasterPageFile="~/Site.master" CodeBehind="Edit.aspx.cs"
Inherits="WebApplication.DynamicData.PageTemplates.Edit" %>
<asp:Content ContentPlaceHolderID="main" runat="Server">
<asp:DynamicDataManager runat="server">
<DataControls>
<asp:DataControlReference ControlID="formView" />
</DataControls>
</asp:DynamicDataManager>
<h2>Edit entry from table <%= table.DisplayName %></h2>
<asp:UpdatePanel runat="server">
<ContentTemplate>
<asp:ValidationSummary runat="server" HeaderText="List of validation errors"/>
<asp:DynamicValidator runat="server" ControlToValidate="formView"
Display="None"/>
<asp:FormView runat="server" ID="formView" DataSourceID="dataSource"
DefaultMode="Edit" OnItemCommand="FormView_ItemCommand"
OnItemUpdated="FormView_ItemUpdated">
<EditItemTemplate>
<asp:DynamicEntity runat="server" Mode="Edit" />
<div class="DDFormActions">
<asp:LinkButton runat="server" CommandName="Update" Text="Update" />
<asp:LinkButton runat="server" CommandName="Cancel" Text="Cancel"
CausesValidation="false" />
</div>
</EditItemTemplate>
</asp:FormView>
<asp:EntityDataSource ID="dataSource" runat="server" EnableUpdate="true" />
<asp:QueryExtender TargetControlID="dataSource" runat="server">
<asp:DynamicRouteExpression />
</asp:QueryExtender>
</ContentTemplate>
</asp:UpdatePanel>
</asp:Content>
The Edit page also includes an EntityDataSource
and QueryExtender
controls to implement data access logic as well as a FormView
and DynamicEntity
controls to render the user interface. However, unlike the read-only Details page template, which allows deleting the current entity, the Edit page template enables you to edit the current entity, but not delete it. Consequently, it sets the Mode
property of the FormView
and DynamicEntity
controls to Edit and sets EnableUpdate
property of the EntityDataSource
control to true
.
As you recall from the discussion of entity templates in Chapter 5, “Filter Templates,” setting the Mode
of the DynamicEntity
control to Edit
instructs this control to choose an appropriate edit-mode entity template, which includes editable controls, like TextBox
and DropDownList
instead of the read-only Literal
and DynamicHyperLink
controls generated by the read-only entity templates.
Setting DefaultMode
of the FormView
control to Edit makes it instantiate the EditItemTemplate
, which contains the DynamicEntity
control along with two LinkButton
controls. The FormView
control has built-in support for the Update command, so when the user clicks the Update link, the current property values of the entity displayed by the FormView
are automatically validated and submitted to the EntityDataSource
control to be saved in the database. The Cancel link is used to abandon editing the current entity and go back to the List page.
The Edit page template includes the ValidationSummary
and DynamicValidator
controls. Although these controls are also included in the Details page template, they haven’t been discussed because validation plays a much more prominent role in the process of editing entities compared to deletion. As you recall from the discussion of field templates in Chapters 4, the individual field values are validated by the field templates with the help of the ASP.NET validation controls, including the new DynamicValidator
control, which enforces validation rules based on the Data Annotation attributes. If any validation errors are encountered, the validators embedded in the field templates indicate invalid fields by displaying an asterisk next to the corresponding data entry controls. However, the complete error messages are displayed by the ValidationSummary
control on the Edit page template.
In field templates, the DynamicValidator
controls are associated with MetaColumn
objects describing specific entity properties. However, the DynamicValidator
control used by the Edit page template is not associated with any particular column and specifies the FormView
as its ControlToValidate
. When configured this way, the DynamicValidator
control catches unhandled validation exceptions thrown by the EntityDataSource
associated with the FormView
. Chapter 8, “Implementing Entity Validation,” provides detailed discussion of this topic.
Listing 6.4 shows the code-behind file of the Edit page template.
using System;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
namespace WebApplication.DynamicData.PageTemplates
{
public partial class Edit : System.Web.UI.Page
{
protected MetaTable table;
protected void Page_Init(object sender, EventArgs e)
{
this.table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
this.Title = table.DisplayName;
this.dataSource.EntityTypeFilter = this.table.EntityType.Name;
this.dataSource.Include = this.table.ForeignKeyColumnsNames;
}
protected void FormView_ItemCommand(object sender, FormViewCommandEventArgs e)
{
if (e.CommandName == DataControlCommands.CancelCommandName)
this.Response.Redirect(this.table.ListActionPath);
}
protected void FormView_ItemUpdated(object sender, FormViewUpdatedEventArgs e)
{
if (e.Exception == null ¦¦ e.ExceptionHandled)
this.Response.Redirect(this.table.ListActionPath);
}
}
}
Initialization code in the Page_Init
event handler is identical to that in the Details page template. The FormView_ItemUpdated
event handler redirects the user to the List page after he clicks the Update link on the Edit page and the modified entity has been successfully saved in the database. The FormView_ItemCommand
event handler also redirects the user to the List page, but in response to him clicking the page’s Cancel link.
The List page template generates web pages that display a list of entities and allow searching and sorting the list by individual columns. Figure 6.4 shows an example of such a page generated for the Products table. At the top of the page, a dynamically generated set of filter controls is displayed for all properties of the entity type for which Dynamic Data has appropriate filter templates. The list of entities occupies the main part of the page. Changing any of the filter controls automatically refreshes the list of entries. The grid columns can be sorted in ascending or descending order by clicking their headers; a set of action links for editing, deleting, or displaying details of individual entity instances is displayed in the left-most column for each row. At the bottom of the grid, there are controls for navigating between pages and changing the number of rows displayed per page.
The Edit and Details links point to the dynamic Edit and Details pages discussed earlier. Examples of these pages can be found in Figures 6.3 and 6.2, respectively. The foreign key navigation properties, such as the Category and Supplier, are also displayed in the form of hyperlinks that, when clicked, take users to the corresponding Details pages. One-to-many navigation properties, such as the Order Items, take users to the List page of the target entity where the search criteria has been prepopulated to display only the child entities of the parent entity clicked. For example, when a user clicks View Order Items of the product with the name Chai, an Order Items list page is displayed with the Product filter control preset to Chai, as shown in Figure 6.5.
The List page template is implemented in List.aspx
, located in the DynamicDataPageTemplates
folder of the web application. Listing 6.5 shows the markup of the built-in template after some cleanup to improve readability and highlight important sections in bold.
The structure of the List page template is actually quite similar to the structure of the Details page template. It also includes DynamicDataManager
, EntityDataSource
, and QueryExtender
controls. The only significant differences are the GridView
control, which this template uses to display entities, and the QueryableFilterRepeater
used to generate the filter controls.
<%@ Page Language="C#" MasterPageFile="~/Site.master" CodeBehind="List.aspx.cs"
Inherits="WebApplication.DynamicData.PageTemplates.List" %>
<%@ Register Src="~/DynamicData/Content/GridViewPager.ascx"
TagName="GridViewPager" TagPrefix="dd" %>
<asp:Content ContentPlaceHolderID="main" runat="Server">
<asp:DynamicDataManager runat="server">
<DataControls>
<asp:DataControlReference ControlID="gridView" />
</DataControls>
</asp:DynamicDataManager>
<h2><%= table.DisplayName %></h2>
<asp:UpdatePanel runat="server">
<ContentTemplate>
<div class="DD">
<asp:ValidationSummary runat="server" HeaderText="Validation errors" />
<asp:DynamicValidator runat="server" ControlToValidate="gridView"
Display="None"/>
<asp:QueryableFilterRepeater runat="server" ID="FilterRepeater">
<ItemTemplate>
<asp:Label runat="server" Text='<%# Eval("DisplayName") %>'
OnPreRender="Label_PreRender" />
<asp:DynamicFilter runat="server" ID="DynamicFilter"
OnFilterChanged="DynamicFilter_FilterChanged" />
<br />
</ItemTemplate>
</asp:QueryableFilterRepeater>
<br />
</div>
<asp:GridView ID="gridView" runat="server" DataSourceID="dataSource"
EnablePersistedSelection="true" AllowPaging="True" AllowSorting="True">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<asp:DynamicHyperLink runat="server" Action="Edit" Text="Edit" />
<asp:LinkButton runat="server" CommandName="Delete" Text="Delete"
OnClientClick='return
confirm("Are you sure you want to delete this item?");'/>
<asp:DynamicHyperLink runat="server" Text="Details" />
</ItemTemplate>
</asp:TemplateField>
</Columns>
<PagerTemplate>
<dd:GridViewPager runat="server" />
</PagerTemplate>
</asp:GridView>
<asp:EntityDataSource ID="dataSource" runat="server" EnableDelete="true" />
<asp:QueryExtender TargetControlID="dataSource" runat="server">
<asp:DynamicFilterExpression ControlID="FilterRepeater" />
</asp:QueryExtender>
<div class="DDBottomHyperLink">
<asp:DynamicHyperLink ID="insertLink" runat="server" Action="Insert">
<img runat="server" src="~/DynamicData/Content/Images/plus.gif"/>
Insert new item
</asp:DynamicHyperLink>
</div>
</ContentTemplate>
</asp:UpdatePanel>
</asp:Content>
The GridView
control is configured to allow paging and sorting. It includes a single TemplateField
to define a column that displays action links—Edit, Delete, and Details. The actual data columns are generated dynamically by a DefaultAutoFieldGenerator
. During page initialization, the DynamicDataManager
control sets the ColumnsGenerator
property of the GridView
with a new instance of the DefaultAutoFieldGenerator
class. So, instead of being specified explicitly in page markup, as shown in the example below, the DynamicField
instances are created dynamically, based on the metadata information describing properties of the current entity type.
<Columns>
<asp:DynamicField DataField="ProductName"/>
<asp:DynamicField DataField="UnitsInStock"/>
</Columns>
Paging functionality is implemented by the GridViewPager
control. This is not a built-in ASP.NET control; it is a user control included in the Dynamic Data project templates of Visual Studio and created in the DynamicDataContent
folder of your web application when you first create the project.
The QueryableFilterRepeater
control, which was discussed in Chapter 5, is used to generate a set of DynamicFilter
controls, one for each property that can be filtered using filter templates available in the DynamicDataFilters
folder of the web application. Notice that this markup does not specify the DataField
property of the DynamicFilter control. This is done automatically when the QueryableFilterRepeater
instantiates its ItemTemplate
for each filterable property in the entity type. The ID
property of the DynamicFilter
control has to be "DynamicFilter
,"
which is what the QueryableFilterRepeater
looks for by default.
The QueryableFilterRepeater
control is associated with the QueryExtender
by the DynamicFilterExpression
in the page markup. In its turn, the QueryExtender
control is associated with the EntityDataSource
, which allows the individual filter templates to modify the LINQ query it generates at run time based on the values selected by the user.
Listing 6.6 shows the code-behind file of the List page template, again, cleaned-up to improve readability and highlight important points.
using System;
using System.Collections.Generic;
using System.Web.DynamicData;
using System.Web.Routing;
using System.Web.UI.WebControls;
namespace WebApplication.DynamicData.PageTemplates
{
public partial class List : System.Web.UI.Page
{
protected MetaTable table;
protected void Page_Init(object sender, EventArgs e)
{
this.table = DynamicDataRouteHandler.GetRequestMetaTable(this.Context);
this.Title = this.table.DisplayName;
this.dataSource.EntityTypeFilter = this.table.EntityType.Name;
this.dataSource.Include = table.ForeignKeyColumnsNames;
IDictionary<string, object> columnValues =
this.table.GetColumnValuesFromRoute(this.Context);
this.gridView.SetMetaTable(this.table, columnValues);
this.insertLink.NavigateUrl = this.table.GetActionPath(
PageAction.Insert, new RouteValueDictionary(columnValues));
}
protected void Label_PreRender(object sender, EventArgs e)
{
var label = (Label)sender;
var dynamicFilter = (DynamicFilter)label.FindControl("DynamicFilter");
var template = dynamicFilter.FilterTemplate as QueryableFilterUserControl;
if (template != null && template.FilterControl != null)
{
label.AssociatedControlID =
template.FilterControl.GetUniqueIDRelativeTo(label);
}
}
protected void DynamicFilter_FilterChanged(object sender, EventArgs e)
{
this.gridView.PageIndex = 0;
}
}
}
The Page_Init
event handler performs the familiar tasks of finding the MetaTable
describing the current entity type using information in the URL route, updating the page title with the display name of the entity, and configuring the EntityDataSource
. However, this code also performs an important additional step, which was not necessary in the Details and Edit page templates discussed so far.
The code in the Page_Init
event handler calls the GetColumnValuesFromRoute
method of the MetaTable
class to obtain a dictionary of property names and values from the request URL. This is important when the user navigates to a list page by clicking a “View Children” link on any of the other dynamically generated pages, such as the View Order Items link on the Product list page in Figure 6.4. In this scenario, the primary key of the parent entity is passed to the page template in a request URL similar to the one shown here:
http://localhost:49212/Order_Details/List.aspx?ProductID=1
When the dictionary of property names and values has been obtained, the Page_Init
code calls the SetMetaTable
extension method defined in the DynamicDataExtensions
class to associate both the MetaTable
object and the dictionary of what it calls default values with the GridView
control. The filter templates rely on this information to provide initial values in filter controls (see the DefaultValue
property of the QueryableFilterUserControl
class).
Normally it is not necessary to call the SetMetaTable
extension method if you have a DynamicDataManager
control associated with a databound control, such as the GridView
in this example. The DynamicDataManager
calls this method automatically, but in this case, the template calls it explicitly to pass the default values to filter templates.
The rest of the code in the Page_Init
event handler deals with initializing the Insert new item hyperlink at the bottom of the page. Using the same dictionary of property names and values, this code generates a link to the dynamic Insert page, with the default values in the URL. As you see shortly, the Insert page template also supports column values passed through the request URL and relies on them to prepopulate some of the field values.
The Label_PreRender
method handles the PreRender
event of the Label
controls generated by the QueryableFilterRepeater
. It associates each label with the filter control generated by its matching DynamicFilter
. This makes the generated label behave like a true control label as opposed to regular selectable text content on the page. Clicking a label in the web browser automatically selects the corresponding control.
The DynamicFilter_FilterChanged
method handles the FilterChanged
event of the DynamicFilter
controls, which fires any time the user changes the filter criteria. Although the QueryableFilterRepeater
automatically notifies the EntityDataSource
control and causes the query to be reloaded, this method flips the GridView
to the first page to avoid confusion and errors when the new filter criteria makes the number of pages in the result set less than the current page number.
The Insert page template is the last of the built-in page templates Dynamic Data projects provide out of the box. This page template generates web pages that allow users to create new entity instances, such as the Add Product page shown in Figure 6.6. From a user interface and behavior standpoint, the Insert pages are similar to the Edit pages discussed earlier in this chapter. However, they are integrated with the List pages, and it will be easier to discuss the Insert page template now that you have reviewed implementation of its List cousin.
One subtle but important difference between Insert and Edit pages is the meaning of the query parameters in their URLs. For the Edit pages, the URL contains the names and values of the primary key properties of an existing entity. For the Insert pages, however, either there are no parameters, or the URL contains the default values for the foreign key properties of the new entity to be created. Notice how in Figure 6.6, the URL for the Add Product page contains a SupplierID
value of 1
. This is because the page in this screenshot was displayed by clicking the Insert new item link on the Products List page that displayed all products offered by a specific supplier, Exotic Liquids. The List page passed the SupplierID
to the Insert page via the generated Insert new item link. This requires the Insert page template to have logic similar to that of the List page template for extracting parameter values from the request URL and sharing it with the field templates.
The Insert page template is implemented in the Insert.aspx
located in the DynamicDataPageTemplates
folder of the web application. Listing 6.7 shows the markup of this template. Most of it is very similar to the markup of the Edit.aspx
shown in Listing 6.3, except that the FormView
, DynamicEntity
, and EntityDataSource
are used in Insert mode. Because the Insert page does not need to retrieve an existing entity instance from the database, it does not need a QueryExtender
, only the data source control.
<%@ Page Language="C#" MasterPageFile="~/Site.master" CodeBehind="Insert.aspx.cs"
Inherits="WebApplication.DynamicData.PageTemplates.Insert" %>
<asp:Content ContentPlaceHolderID="main" runat="Server">
<asp:DynamicDataManager runat="server">
<DataControls>
<asp:DataControlReference ControlID="formView" />
</DataControls>
</asp:DynamicDataManager>
<h2>Add new entry to table <%= table.DisplayName %></h2>
<asp:UpdatePanel runat="server">
<ContentTemplate>
<asp:ValidationSummary runat="server" HeaderText="List of validation errors"/>
<asp:DynamicValidator runat="server" ControlToValidate="formView"
Display="None" />
<asp:FormView runat="server" ID="formView" DataSourceID="dataSource"
DefaultMode="Insert" OnItemCommand="FormView_ItemCommand"
OnItemInserted="FormView_ItemInserted">
<InsertItemTemplate>
<asp:DynamicEntity runat="server" Mode="Insert" />
<div class="DDFormActions">
<asp:LinkButton runat="server" CommandName="Insert" Text="Insert" />
<asp:LinkButton runat="server" CommandName="Cancel" Text="Cancel"
CausesValidation="false" />
</div>
</InsertItemTemplate>
</asp:FormView>
<asp:EntityDataSource ID="dataSource" runat="server" EnableInsert="true" />
</ContentTemplate>
</asp:UpdatePanel>
</asp:Content>
The most important difference between the code-behinds of the Insert (see Listing 6.8) and Edit (see Listing 6.4) page templates is the additional initialization step the Insert page templates performs for the FormView
control. Just like the List page template, it extracts the names and values of the entity properties from the request URL and associates them with the FormView
control by calling the SetMetaTable
extension method. The field templates generated by the DynamicEntity
control use this information to provide default field values in their data entry controls. The FieldValue
and FieldValueEditString
properties of the FieldTemplateUserControl
class respect the default values associated with their parent databound control, such as the FormView
in this example. This is why the Supplier drop-down list generated by the ForeignKey_Edit field template in Figure 6.6 has been prepopulated with Exotic Liquids.
using System;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
namespace WebApplication.DynamicData.PageTemplates
{
public partial class Insert : System.Web.UI.Page
{
protected MetaTable table;
protected void Page_Init(object sender, EventArgs e)
{
this.table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
this.Title = this.table.DisplayName;
this.dataSource.EntityTypeFilter = table.EntityType.Name;
this.formView.SetMetaTable(this.table,
this.table.GetColumnValuesFromRoute(Context));
}
protected void FormView_ItemCommand(object sender, FormViewCommandEventArgs e)
{
if (e.CommandName == DataControlCommands.CancelCommandName)
Response.Redirect(this.table.ListActionPath);
}
protected void FormView_ItemInserted(object sender, FormViewInsertedEventArgs e)
{
if (e.Exception == null ¦¦ e.ExceptionHandled)
Response.Redirect(this.table.ListActionPath);
}
}
}
The FormView_ItemCommand
event handler in the Insert page template serves the same function the method with this name performs in the Edit page template—redirecting the user back to the List page when she clicks Cancel. The FormView_ItemInserted
event handler also does that when a new entity has been successfully saved in the database.
Dynamic Data does not limit the set of page actions and page templates. You can easily define a new action and create a new page template to handle it. As an example, consider the action of deleting an entity instance. The default page templates support deletion directly in the List and Details pages. When you click the Delete link on one of these pages, a JavaScript prompt window is displayed, asking you to confirm the deletion. Although this approach works, it definitely has a Windows-like feel to it. A more traditional way to do this in a web application is to take the user to a separate page where she can review the entity she is about to delete and confirm the action.
Listing 6.9 shows the page template that implements the Delete page action. It is almost identical to the built-in Details page template. Just like the other page templates, this ASPX file needs to be placed in the DynamicDataPageTemplates
folder of the web application project, in a file called Delete.aspx.
<%@ Page Language="C#" MasterPageFile="~/Site.master" CodeBehind="Delete.aspx.cs"
Inherits="WebApplication.DynamicData.PageTemplates.Delete" %>
<asp:Content ContentPlaceHolderID="main" runat="server">
<asp:DynamicDataManager runat="server">
<DataControls>
<asp:DataControlReference ControlID="formView" />
</DataControls>
</asp:DynamicDataManager>
<h2>Are you sure you want to delete this from table <%= table.DisplayName %>?</h2>
<asp:UpdatePanel runat="server">
<ContentTemplate>
<asp:ValidationSummary runat="server" HeaderText="List of validation errors"/>
<asp:DynamicValidator runat="server" ControlToValidate="formView"
Display="None"/>
<asp:FormView ID="formView" runat="server" DataSourceID="dataSource"
OnItemDeleted="FormView_ItemDeleted">
<ItemTemplate>
<asp:DynamicEntity runat="server" />
<div class="DDFormActions">
<asp:Button runat="server" CommandName="Delete" Text="Delete"/>
<asp:DynamicHyperLink runat="server" Action="List" Text="Back to list"/>
</div>
</ItemTemplate>
</asp:FormView>
<asp:EntityDataSource ID="dataSource" runat="server" EnableDelete="true" />
<asp:QueryExtender TargetControlID="dataSource" runat="server">
<asp:DynamicRouteExpression />
</asp:QueryExtender>
</ContentTemplate>
</asp:UpdatePanel>
</asp:Content>
The only substantial difference between this new Delete template and the built-in Details template is the set of button and link controls. The new Delete template uses a Button
control instead of a LinkButton
to make confirmation of deletion visually different from merely requesting it. It also does not have an Edit link the Details page template uses to take users to the Edit page.
Although they might seem unnecessary, the ValidationSummary
and DynamicValidator
controls still play an important role in the Delete page template. If the user tries to delete a Customer who placed one or more orders, a database error will occur due to a foreign key constraint violation. As discussed in Chapter 8, database errors can be presented to the Dynamic Data as instances of the ValidationException
class. The DynamicValidator
control at the top of the page automatically displays unhandled ValidationException
errors in the ValidationSummary
instead of generating a yellow screen of death.
After implementing the new Delete page template, you also need to modify the existing templates to redirect the user to a new dynamic page generated by the new template instead of displaying the JavaScript prompt. In both List and Details page templates, replace the following LinkButton
:
<asp:LinkButton runat="server" CommandName="Delete" Text="Delete"
OnClientClick='return confirm("Are you sure you want to delete this
item?");'/>
with the following DynamicHyperLink
control:
<asp:DynamicHyperLink runat="server" Action="Delete" Text="Delete" />
And because the Details page will no longer delete the records, you can remove the EnableDelete
property from its EntityDataSource
control in page markup and remove the ItemDeleted
event handler from the FormView
control in the code-behind.
Figure 6.7 shows an example of the dynamically generated Delete page for the Product entity type. The entity itself (labels and field values) looks similar to the dynamically generated Details page, which is a great example of how page templates allow you to implement different actions and data access logic while reusing the common presentation logic encapsulated by the entity templates.
The page templates discussed so far are dynamic, meaning they can generate web pages for any entity type in your data model. Thanks to the rich metadata information provided by Dynamic Data in the form of a MetaTable
object, a single generic page template can handle a particular type of action, such as List or Details, for any entity type. The generated web pages vary depending on the properties of the entity type, but the variations are limited to the order of fields on the form, their labels, and data entry controls. When the user interface cannot be implemented by creating a custom entity template, developers can create custom page templates to implement unique functionality required for a particular entity type.
Consider the Orders table in the Northwind sample database. It stores order header information, such as Order Date, Customer, Shipper, and so on. However, the order items are stored in a separate table called Order Details. In the sample data model, these tables are represented by the Order
and Order_Detail
classes, respectively. Intuitively, an Order_Detail
instance is a part of the parent Order instance and cannot be entered independently. This is different from how the Order and Customer instances relate to each other: An Order is associated with a Customer and can be entered independently. Unfortunately, Dynamic Data cannot distinguish between these types of relationships automatically and has to assume that the Order_Detail
is an independent entity type. As a result, the default experience for viewing orders is rather cumbersome because it requires users to switch between the Order details page and the Order_Detail List page shown in Figure 6.8.
Ideally, users should be able to see an entire order on a single page, similar to that shown in Figure 6.9. In a Dynamic Data web application, this can be accomplished by creating a custom Details page template for the Order entity type.
Dynamic Data looks for custom page templates under DynamicDataCustomPages
, in a subfolder with the name that matches the name of the MetaTable
describing its entity type. Here is how the folder structure looks:
DynamicData
CustomPages
{Table1}
{Template1}.aspx
{Template2}.aspx
...
{Table2}
{Template1}.aspx
{Template2}.aspx
...
...
PageTemplates
{Template1}.aspx
{Template2}.aspx
...
To create a custom Details page template for the Order entity type, you need to create a subfolder called Orders
(note the plural form, as in the name of the table). Begin implementing the custom page by copying the default Details.aspx
page template from the PageTemplates
folder to the newly created CustomPagesOrders
folder. This gives you the FormView
control that will display the Order information along with the EntityDataSource
and QueryExtender
controls it needs to be able to get the data from the database. The second part of the page needs a GridView
control to display Order Details plus its own EntityDataSource
and QueryExtender
controls. Finally, a Literal
control to display the order total calculated in the code-behind. Listing 6.10 shows the complete markup of the new page.
<%@ Page Language="C#"
MasterPageFile="~/Site.master" CodeBehind="Details.aspx.cs"
Inherits="WebApplication.DynamicData.CustomPages.Orders.Details" %>
<asp:Content ContentPlaceHolderID="main" runat="Server">
<asp:DynamicDataManager runat="server">
<DataControls>
<asp:DataControlReference ControlID="formView" />
</DataControls>
</asp:DynamicDataManager>
<table>
<tr valign="top">
<td>
<h2>Entry from table <%= ordersTable.DisplayName %></h2>
<asp:FormView runat="server" ID="formView" DataSourceID="formDataSource">
<ItemTemplate>
<asp:DynamicEntity runat="server" />
<div class="DDFormActions">
<asp:DynamicHyperLink runat="server" Action="Edit" Text="Edit" />
<asp:DynamicHyperLink runat="server" Action="Delete" Text="Delete" />
<asp:DynamicHyperLink runat="server" Action="List" Text="List"/>
</div>
</ItemTemplate>
</asp:FormView>
<asp:EntityDataSource ID="formDataSource" runat="server"/>
<asp:QueryExtender TargetControlID="formDataSource" runat="server">
<asp:DynamicRouteExpression />
</asp:QueryExtender>
</td>
<td>
<h2><%= itemsTable.DisplayName %></h2>
<asp:GridView ID="gridView" runat="server" DataSourceID="gridDataSource"
AutogenerateColumns="false" AllowSorting="True">
<Columns>
<asp:DynamicField DataField="Product" />
<asp:DynamicField DataField="UnitPrice"/>
<asp:DynamicField DataField="Quantity"/>
<asp:TemplateField>
<ItemTemplate>
<asp:DynamicHyperLink runat="server" Action="Edit" Text="Edit" />
<asp:DynamicHyperLink runat="server" Action="Delete" Text="Delete" />
<asp:DynamicHyperLink runat="server" Action="Details" Text="Details"/>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<asp:EntityDataSource ID="gridDataSource" runat="server"
ConnectionString="name=NorthwindEntities"
DefaultContainerName="NorthwindEntities"
EntitySetName="Order_Details" Include="Product"
OnSelected="GridDataSource_Selected" />
<asp:QueryExtender TargetControlID="gridDataSource" runat="server">
<asp:DynamicRouteExpression />
</asp:QueryExtender>
Total: <asp:Literal ID="totalLiteral" runat="server"/>
</td>
</tr>
</table>
</asp:Content>
Note that unlike in the generic List page template, the GridView
in this custom page does not rely on the DynamicDataManager
control for initialization. The main reason is that the DynamicDataManager
determines which MetaTable
to associate with its target controls based on the information in the URL route. In this case, the URL route contains the table name Orders, but you need to associate the GridView
control with the Order Details table and need to do this in the code-behind, as shown in Listing 6.11.
You also do not want to rely on the DynamicDataManager
to initialize the GridView
control because we need a specific set of columns to display the order details. You could display them all by assigning the DefaultAutoFieldGenerator
to the ColumnsGenerator
property of the GridView
control; however, this would also include the Order column, which you can see on the default List page of the Order Details table back in Figure 6.8. Because the entire Order will be already displayed on the new page, including this column would be redundant. Although you could hide it by manipulating the entity metadata, it is important to remember that this custom page template does not have to be dynamic. This task is much easier to accomplish by specifying the required Columns
explicitly in the page markup, as the DynamicField
instances for the Product
, UnitPrice
, and Quantity
properties we want to display.
Another important difference here is that initialization of the gridDataSource
instance of the EntityDataSource
control is performed directly in markup, with ConnectionString
, DefaultContainerName
, EntitySetName
, and Include
properties being “hard-coded” for the purposes of this custom page, where you know exactly which entity it needs to query and don’t need to make the code dynamic. However, this code continues to rely on the DynamicRouteExpression
in the second QueryExtender
control, which conveniently limits the set of Order Details instances retrieved from the database based on the OrderID column value extracted from the page request URL.
using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Linq;
using System.Web.DynamicData;
using System.Web.UI.WebControls;
using DataModel;
namespace WebApplication.DynamicData.CustomPages.Orders
{
public partial class Details : System.Web.UI.Page
{
protected MetaTable ordersTable;
protected MetaTable itemsTable;
protected void Page_Init(object sender, EventArgs e)
{
this.ordersTable = DynamicDataRouteHandler.GetRequestMetaTable(this.Context);
this.Title = this.ordersTable.DisplayName;
this.formDataSource.Include = this.ordersTable.ForeignKeyColumnsNames;
this.itemsTable = Global.DefaultModel.GetTable(typeof(Order_Detail));
this.gridView.SetMetaTable(this.itemsTable);
}
protected void GridDataSource_Selected(object sender,
EntityDataSourceSelectedEventArgs e)
{
IEnumerable<Order_Detail> orderItems = e.Results.Cast<Order_Detail>();
decimal totalAmount = orderItems.Sum(item => item.UnitPrice * item.Quantity);
this.totalLiteral.Text = totalAmount.ToString();
}
}
}
The Page_Init
event handler performs the remaining initialization steps, similar to how this is done in the dynamic page templates. The first part of this method still relies on the GetRequestMetaTable
method of the DynamicDataRouteHandler
class to retrieve the Orders MetaTable
using information in the URL route. This was simply carried over from the dynamic Details page template. On the other hand, the Order Details MetaTable
has to be obtained directly from the default MetaModel
object registered in the Global.asax
. This MetaTable
object (itemsTable
) is then associated with the GridView
control to provide the DynamicField
instances access to the metadata information they need to instantiate appropriate field templates.
The GridDataSource_Selected
method handles the Selected
event of the EntityDataSource
control that retrieves instances from the Order Details table. It calculates the total order amount as a sum of UnitPrice values multiplied by the Quantity of the individual order items. The result is then displayed in the Literal
control on the page.
Dynamic Data web applications rely on the general-purpose URL routing capabilities built into ASP.NET. Routes are typically configured at the application start time, in the Application_Start
event handler located in the Global.asax
file.
RouteTable.Routes.Add(new DynamicDataRoute("{table}/{action}.aspx"));
As discussed in the beginning of this chapter, {table}
and {action}
are placeholders for the two route parameters that have special meaning in ASP.NET Dynamic Data. The first parameter supplies the name of the table whose data needs to be displayed by a dynamic page, and the second determines what kind of page needs to be displayed, or perhaps more precisely, how the table data needs to be presented to the user. Dynamic Data expects the table parameter to match the name of one of the MetaTable
objects that describe entity types registered in the global MetaModel
, whereas the action parameter provides the information necessary to find the appropriate page template to handle the request. The route definition just shown allows Dynamic Data to determine that the following page request URL applies to the table called Products and the page template called List:
http://localhost/Products/List.aspx
Routes do not have to include the action parameter as a part of the URL itself. Instead, it can be specified by explicitly setting the Action
property of the DynamicDataRoute
class as shown here:
RouteTable.Routes.Add(new DynamicDataRoute("{table}")
{
Action = PageAction.List
});
Given this route definition, the URL for displaying a list of products will not include the name of the page template at all and look like this instead:
http://localhost/Products
Similarly, the table parameter does not have to come from the URL either; it can be specified using the Table
property of the DynamicDataRoute
class. Notice how in the following example, the route definition does not include any parameters and simply hard-codes the URL to "Product"
(singular), which does not match the plural name of the table.
RouteTable.Routes.Add(new DynamicDataRoute("Product")
{
Action = PageAction.Details,
Table = "Products"
});
Based on this route definition, the Dynamic Data web application can use the following URL:
http://localhost/Product?ProductID=1
instead of the more verbose and less user-friendly:
http://localhost/Products/Details.aspx?ProductID=1
URL parameters are an alternative to the Query parameters used by traditional ASP.NET WebForms applications. Whereas the traditional query parameters require both names and values to be present in the query string, the URL parameters allow web applications to determine parameter names automatically, based on the position of their values in the URL. Consider the following route, which replaces the ProductID query parameter with a positional URL parameter:
RouteTable.Routes.Add(new DynamicDataRoute("Product/{ProductID}")
{
Action = PageAction.Details,
Table = "Products"
});
This route definition allows the web application to implement the product catalog without query parameters and support URLs similar to that shown below. This not only makes the URLs shorter and easier for users to read, but also makes it easier for the search engines to index dynamic pages, which is very important for public-facing web sites:
http://localhost:49212/Product/3
Dynamic Data still supports traditional Query parameters, which you can activate by registering a route that does not include any URL parameters and does not specify parameter values explicitly:
RouteTable.Routes.Add(new DynamicDataRoute("Dynamic"));
The route registered just shown makes Dynamic Data use URLs that look like this instead:
http://localhost/Dynamic?Table=Products&Action=List
Routes have a dual purpose. In addition to helping the ASP.NET runtime determine which ASPX page will be used to handle a particular incoming request, they also determine how hyperlinks pointing to the dynamic pages will be generated. There are several different methods for generating hyperlinks in ASP.NET web applications. One of them involves the DynamicHyperLink
control supplied by Dynamic Data. This control inherits from the ASP.NET HyperLink
control and allows you to generate hyperlinks to dynamic pages. In the following example, the name of the table and the required action are explicitly specified.
<asp:DynamicHyperLink runat="server"
ContextTypeName="DataModel.NorthwindEntities"
TableName="Products" Action="List" Text="Show all" />
The TableName
property specifies the name of the MetaTable
describing the target entity, and the ContextTypeName
specifies the name of the context class where the target entity is defined. Depending on how the routing is configured, this control generates different URLs without you having to modify any of its markup. Table 6.1 shows examples of the different route definitions and URLs this control would generate based on them.
The DynamicHyperLink
control can also be databound to an entity instance, in which case it can automatically determine the ContextTypeName
and the TableName
and will generate URLs that include names and values of its primary key columns in the query portion of the address. The Details page template discussed earlier takes advantage of this functionality by including the DynamicHyperLink
controls as part of the ItemTemplate
in the FormView
control as shown in the following extract:
<asp:FormView runat="server" ID="formView" DataSourceID="dataSource">
<ItemTemplate>
<asp:DynamicEntity runat="server" />
<div class="DDFormActions">
<asp:DynamicHyperLink runat="server" Action="Edit" Text="Edit" />
<asp:DynamicHyperLink runat="server" Action="Delete" Text="Delete" />
<asp:DynamicHyperLink runat="server" Action="List" Text="Show all"/>
</div>
</ItemTemplate>
</asp:FormView>
Notice how the DynamicHyperLink
controls in this example do not have the ContextTypeName
and TableName
properties specified. The FormView
control databinds its ItemTemplate
to the entity instance provided by the associated EntityDataSource
control. The controls locate the MetaTable
object based on the entity type and generate URLs that look like this:
http://localhost/Products/Edit.aspx?ProductID=1
http://localhost/Products/Delete.aspx?ProductID=1
http://localhost/Products/List.aspx?ProductID=1
Dynamic hyperlinks can be also generated with the help of the RouteUrl
markup expression. This is a special syntax that ASP.NET parser recognizes and uses to generate a dynamic URL based on parameter values you specify:
<asp:HyperLink runat="server" Text="Show all"
NavigateUrl="<%$RouteUrl:table=Products, action=List%>" />
Under the hood, both the RouteUrl
markup expression and the DynamicHyperLink
control rely on the low-level API the ASP.NET provides for routing. The next code sample illustrates how they work by creating a dynamic hyperlink programmatically. It creates a RouteValueDictionary
to hold the Table
and Action
parameter values and then calls the GetVirtualPath
method of the global Routes
collection to generate the URL in a form of a virtual path:
HyperLink hyperLink = // ...
RequestContext context =
DynamicDataRouteHandler.GetRequestContext(HttpContext.Current);
RouteValueDictionary routeValues = new RouteValueDictionary();
routeValues["table"] = "Products";
routeValues["action"] = "List";
VirtualPathData pathData = RouteTable.Routes.GetVirtualPath(
context, routeValues);
hyperLink.NavigateUrl = pathData.VirtualPath;
Although you will probably never need to write code that creates dynamic hyperlinks programmatically, it is important to understand how they are generated by the ASP.NET runtime. In particular, notice the Routes
collection is used to generate a dynamic URL and not a single particular route object. This is significant because the Routes
collection evaluates routes one by one, in the order they were registered, looking for the route matching the supplied set of parameter values. As soon as the match is found, the further evaluation is stopped, and the matching route is used to generate the URL.
A similar evaluation process occurs when the ASP.NET runtime needs to find a page template to handle an incoming web request. It starts with the route that was registered first and goes down the list until it finds the first match. All other routes that were registered after the first match are ignored.
Keeping in mind how the route evaluation process works, register the most specific routes first, leaving the more generic routes until the end. For example, if you had registered the following two routes in this order, your web application would always ignore the second route when generating dynamic hyperlinks:
RouteTable.Routes.Add(new DynamicDataRoute("{table}/{action}.aspx"));
RouteTable.Routes.Add(new DynamicDataRoute("{table}")
{
Action = PageAction.List
});
In other words, a Product list hyperlink would always be presented to users as http:// localhost/Products/List.aspx
and never as http://localhost/Products
. However, when handling incoming requests, the second URL would not match the first route, “fall through,” and still get picked up by the second route.
Changing the order of route registrations gives you a fair amount of control over the URL resolution process. However, when it is insufficient, you can use route constraints to make it more specific. Route constraints are defined in the Constraints
dictionary of a route object as pairs of two string values, where first is a parameter name, and second is a regular expression pattern that the parameter value must match. The following example defines a route where the table parameter value can be either Products
or Orders
:
RouteTable.Routes.Add(new DynamicDataRoute("{table}")
{
Action = PageAction.List,
Constraints = new RouteValueDictionary(){{"table","Products¦Orders"}}
});
RouteTable.Routes.Add(new DynamicDataRoute("{table}/{action}.aspx"));
Based on the route definitions in this last example, only the Products and Orders tables will have the List URLs generated as http://localhost/Products
or http://localhost/Orders
. All other tables will use the second route and have URLs similar to http://localhost/Suppliers/List.aspx
.
In the examples of URL routing discussed so far, the action parameter has always matched the name of the page template configured to handle it, such as the Details.aspx page template, which handled the Details page action, and the List.aspx page template, which handled the List action. Having a page template implement a single action makes it focused and easy to maintain. Compared to a traditional ASP.NET WebForms approach (where a single page often tries to support all three item-level actions—Insert, Edit, and Details), single-action page templates are usually simpler. However, you are not limited to having one-to-one relationships between actions and page templates.
If this makes sense in your application, you can have a single page template handle multiple actions. Visual Studio project templates for Dynamic Data web applications include an example of such a page template called ListDetails.aspx
. This template combines a GridView
control similar to the one in the List page template and a FormView
control that displays details of the row currently selected in the grid. This page template can be registered to handle both List and Details actions as shown in this example:
RouteTable.Routes.Add(new DynamicDataRoute("{table}")
{
Action = PageAction.List,
ViewName = "ListDetails"
});
RouteTable.Routes.Add(new DynamicDataRoute("{table}")
{
Action = PageAction.Details,
ViewName = "ListDetails"
});
It is actually the ViewName
property of the DynamicDataRoute
class that determines the name of the page template that will be used to handle the request. If the ViewName
was not provided by the route definition, Dynamic Data will assume it to be the same as the Action
parameter. Separation of Action
from ViewName
allows Dynamic Data web applications to change implementation of page templates without breaking the navigation in existing ASPX pages.
Figure 6.10 summarizes the algorithm Dynamic Data uses to find an appropriate page template and handle an incoming web request. Most of the power and flexibility in this process comes from the ASP.NET routing.
A Dynamic Data web application first uses the registered URL routes to determine the Table
and the Action
names. When the matching route is found, unless it provides an explicit ViewName
, the Action
name is used instead to identify the name of the page template.
Having determined both Table
and ViewName
from the matching route, Dynamic Data first tries to find a matching custom page template in a subfolder with the name of the table inside of the DynamicDataCustomPages
. If a custom template exists, it is immediately used to handle the incoming web request.
If a matching custom page template does not exist, Dynamic Data then tries to find a dynamic page template in the DynamicDataPageTemplates
folder of the web application. If neither custom nor dynamic page template with the matching ViewName
can be found, an HTTP error 404 (Resource cannot be found) is returned back to the user.
Page templates are responsible for generating web pages that perform CRUD and other generic actions. A single set of page templates can be reused to implement operations for multiple entity types. Out of the box, Dynamic Data web application projects come with page templates for inserting, editing, displaying, and searching for entities of any type. New dynamic page templates can be created to support additional action types, such as deleting an entity.
A page template implements most of the data access and some of the presentation logic required by a dynamic page. Dynamic Data relies on the information provided by ASP.NET routing to determine which page template to use and for which entity type it needs to generate a web page. Dynamic page templates typically rely on special controls, such as DynamicEntity
and DynamicFilter
to generate user interface based on metadata information describing the entity type. When dynamically generated user interface is not sufficient, custom page templates can be used to create a unique user interface required for a particular entity type. Page templates do not replace the regular, custom ASP.NET web pages that still need to be used for screens such as home page, login, dashboards, and so on.
Compared to the traditional multi-purpose WebForms pages, which tend to be verbose and have a large number of simple controls, Dynamic Data page templates are typically much smaller and rely on a number of advanced controls, such as DynamicEntity
, DynamicFilter
, and QueryExtender
. By their nature, dynamic controls are much more abstract than the traditional TextBox
and DropDownList
and therefore can be harder to understand for developers who are new to Dynamic Data.
Dynamic page templates work well for applications that need to display a large number of similar-looking pages, which tends to be the case for internal enterprise applications. Custom page templates, on the other hand, are better suited when unique functionality is required for a particular entity type, such as when implementing a public-facing web site.
3.145.109.234