(Print this page)

ASP.Net 3.5 SP1 Dynamic Data
Published date: Sunday, October 5, 2008
On: Moer and Éric Moreau's web site

Surprise! The old windows guy can also do web! But don’t worry I will come back soon with more articles on Windows programming.

Yes this article is about a web feature, the new Dynamic Data feature that was made officially available in Visual Studio 2008 SP1.

Requirements

You absolutely need Asp.Net 3.5 which is available when you install Visual Studio 2008. But wait, it is not enough! You also need Service Pack 1 for Visual Studio 2008 (details and download link available from here). This feature was added (isn’t a service pack supposed to fix feature and not add them?) with the SP1 because it was not ready when the RTM version of Visual Studio 2008 hit the market.

What it is and what it solves

I like the way ASP.Net explains it:

“ASP.NET Dynamic Data provides a framework that enables you to quickly build a functional data-driven application, based on a LINQ to SQL or Entity Framework data model. It also adds great flexibility and functionality to the DetailsView, FormView, GridView, and ListView controls in the form of smart validation and the ability to easily change the display of these controls using templates.”

In short, I would say that it builds great data-driven web site to administer your data without requiring much coding if you are ready to accept default behaviours.

Almost every time we ear/read about Dynamic Data, we ear/read scaffolding. But what is it? On this topic, I prefer MSDN library definition best:

“Scaffolding is a mechanism that enhances the existing ASP.NET page framework by dynamically displaying pages based on the data model. Scaffolding provides the following capabilities:

  • Minimal or no code to create a data-driven Web application.
  • Quick development time.
  • Built-in data validation based on the database schema.
  • Automatic data selection created for each foreign key or Boolean field.”

Does it work?

I have to admit that I am pretty impressed. With very little code, you can create a data-driven web site ideal for data maintenance. I am not sure I would provide it as the main interface for real web applications but is more than enough for all the administration pages we normally have to provide to maintain all reference tables.

We will see shortly that when you create a Dynamic Data web project, it generates a couple of generic page templates and field templates. We then have to provide a data context and the magic occurs when the application runs. The metadata provided by the data context combined with the generic templates are enough to display, edit and navigate through the tables of the data context.

I have read that all the dynamic cost about 10% in performance which I think is not that bad considering the effort required to put it in place.

My biggest concern is that it requires a LINQ to SQL or an Entity Framework data model as its source. There is no way to set your own business layer as the data source.

Look ma, no hands!

Enough talking, I am sure that you want to see it in action.

So open Visual Studio 2008 SP1 and create a new Dynamic Data Web Application as shown in figure 1. This will result in a LINQ to SQL type of application. The other choice named the Dynamic Data Entities Web Application relies on Entity Framework to provide data.

As you may have notice, I am using a Web Application. I could also have created a Web Site which provides exactly the same 2 templates. My choice is strategic as it will generate an error that is not very well documented on demos of Dynamic Data. I will show you how to solve that specific problem (not too bad for a Windows guy!).

Figure 1: Create a new Dynamic Data Web Application

If you inspect your solution explore, you will find the usual web.config, Default.aspx, a css file, a master page (see figure 2). What is specific to the Dynamic Data template is more in the folder under DynamicData. This is where you will find all the templates that we will customize and enhanced shortly.

Figure 2: The solution explorer

The first thing we need to do after the project has been created is to provide a data source. Because of the template we have selected, we have to provide a LINQ to SQL data source. To add one to the project, right-click the project, select Add, then New Item. Finally, select the LINQ to SQL Classes template and give it a more meaningful name like NW and click the Add button as shown in figure 3 (because like all the good demos out there, I will be using the good old NorthWind database).

Figure 3: Adding the LINQ to SQL class

You will now get the Object Relational Designer that will open empty. Click the Server Explorer link, connect to your SQL Server database and pick your favourite database (mine is NorthWind) and drag all the tables to the designer surface. You can now close the designer as we are done with it. We won’t push LINQ to SQL further, we will use it as is.

Now that we have our data source, we need to let the project know where to find it. Open the Global.asax file and find this line:

' model.RegisterContext(GetType(YourDataContextType), New ContextConfiguration() With {.ScaffoldAllTables = False})

You need to do 3 changes to that line:

  1. Uncomment it (remove the ‘)
  2. Change YourDataContextType for the real name of your data context (NWDataContext if you named your LINQ to SQL class NW as I did).
  3. Replace False with True to tell to scaffold all the tables.

Your application is now ready. It has the templates, the data and the binding between the 2.

Before running a web application I always make sure I set the start page. To set it, open the project properties, go to the Web tab and set the Specific Page field to your start page (Default.aspx in our case) as shown in figure 4.

Figure 4: Setting the start page

Now, hit F5 to start this dynamic application.

If you forget to set something in your Global.asax file, you should get an exception like the one shown in figure 5.

Figure 5: Troubles with Global.asax file

If you did everything correctly, you should see your application starting like shown in figure 6. All the tables you have added to your LINQ to SQL modal will appear on the first page (we will see later how to hide some tables).

Figure 6: The application running without writing a line of code

All those table names are links to some other pages. For example, click the Territories table. It will get you to a URL that looks like this:

http://localhost:4853/Territories/List.aspx 

This shows you a list view of all the data in this table like shown if figure 7. This is some of the things we can see in this page:

  • The List page displays all the data found in the data model.
  • The column headers which are currently set to the column name. I will show you how to change them later in this article.
  • On the left of the grid, it provides links to edit, delete and view the details of a particular item. Clicking the edit or the details link navigates to another page. Clicking the delete link, ask you a confirmation question.
  • If the table has any foreign keys, they are also shown as links on the right of the grid. Clicking them navigates to either a list view (like the EmployeeTerritories link) or to an edit page (like the Region link) depending on the side of the foreign key.
  • The pagination is also done automatically for you if there are more than 10 rows of data.
  • At the top of the grid, there may also be filters if the table uses some other table in foreign keys (like the region here). I have seen concerns in newsgroups about the fact that it can be resources consuming if your source table has thousands of rows!
  • At the bottom of the grid, there is also a link to add a new item which navigates to the Insert.aspx table if you click on it.

Figure 7: The Territories table in list view

If you now click the Edit link, it will show you the page of figure 8 and it will navigate to this URL:

http://localhost:4853/Territories/Edit.aspx?TerritoryID=06897   

Figure 8: the edit page

In figure 8, you can see all the fields. You can see that I have removed the description of the territory and I have an error message. This validation rule comes from the data model. Things like data type, maximum length, nullable attribute are retrieved from the schema and are automatically added to the dynamic pages. Finally, there are Update and Cancel links at the bottom of view.

In a conventional web application (one that you would have crafted yourself), the URL represents the structure of folders and the pages in your project. Look at your project and you will not find any Territories folder. This is the Dynamic magic part. Using the 5 pages created by the template in the DynamicData/PageTemplates folder, everything displays correctly on screen and let us navigates through and edits the data provided by the data model. And because everything so far is automatic, it doesn’t mean that it lets you edit with invalid values (it is as valid as your database schema is explicit).

Global customisation

Now that we have explored the automatic part, let’s see if it is flexible. It’s not because the project created stuff from a template for us that we cannot modify it.

We will start by modifying the master page (Site.master). Open it in the designer and change things like the Title attribute (title of the page), the H1 tag label and maybe set a background color to the page. All those modifications will be reflected in all the pages. You can also open the Site.css file and change styles like the font family, font size, color of just about anything in the web site as all dynamic pages are using this style sheet.

In-place editing

The default way of displaying is that we are first presented a grid of the data with links that bring us to other pages when we click on insert/edit/details links. When you look at the DynamicData/PageTemplates folder, you also see a 5th page that was not used yet: the ListDetails.aspx page. This page offers the in-place editing style.

To use the in-place editing, you need to modify the Global.asax file. Open it and find this code snippet and uncomment it:

'routes.Add(New DynamicDataRoute("{table}/ListDetails.aspx") With { _
'    .Action = PageAction.List, _
'    .ViewName = "ListDetails", _
'    .Model = model})

'routes.Add(New DynamicDataRoute("{table}/ListDetails.aspx") With { _
'    .Action = PageAction.Details, _
'    .ViewName = "ListDetails", _
'    .Model = model})

By un-commenting it, we set the route to the ListDetails page instead of the default List page.

But if you just uncomment it and you run your application, you won’t see any changes to your pages. This is because another route is already telling to use the List Page. Still in the Global.asax file, if you go up a couple lines from the snippet you just uncomment, you will find this one:

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

Routes are a collection and when the application starts, the first one found dictates how the pages are displayed. You can comment this section and all your pages will then switch to the in-place editing. Instead of commenting it out, modify so it reads this way (there are 2 modifications – look for Shippers):

routes.Add(New DynamicDataRoute("Shippers/{action}.aspx") With { _
    .Constraints = New RouteValueDictionary(New With {.Action = "List|Details|Edit|Insert"}), _
    .Model = model, .Table = " Shippers"})

Now rerun the application. If you click on the Shippers link, you will see that it goes to the List page (with links to edit, delete, details, and insert) exactly like the first time we ran the application. Now return to the Territories page you will a somewhat different layout as shown in figure 9. The same validation rules coming from the data model are applicable in this mode. It does not make any differences whatever the display layout you are using.

Figure 9: The ListDetails presentation

Some of the differences include (but are not limited too):

  • The selected row has a yellow background.
  • The selected row is also displayed at the bottom of the grid.
  • The Details link is not available anymore since you already have the details on this page.
  • If you click the Edit link on the row of the grid, you can edit in the grid directly.
  • If you click the Edit link in the details section (at the bottom of the grid), you edit in the details section.
  • To insert a new item, you have to use the New link of the details section.

Customizing the data model

So far, apart from commenting and un-commenting existing lines of code, we haven’t done much code-wise. In earlier steps of the demo, I told you that we were to change the column headers. The time has come to do that and much more. So roll-up your sleeves we will write some code!

What I want to show you now is how to modify the data model to replace the caption of the fields, to hide some fields, to set range validation, to change the display format, to change the edit control, and even to set complex business rules. All those things have to be done at a place that won’t be affected if we ever need to recreate or update our original data model (in case tables and/or fields changed).

First, we need to create a class. The name of the class is very important. For this section of the demo, we will work with the Products table of the Northwind database. When we have created the data model by dragging and dropping the tables from the database to the designer surface, the code generated a class called Product (the code generator removes the trailing “s”). Since we want to extend this class, our own class needs to have the very same name and we won’t get any error since our class will be a partial class (all partial classes having the same name are compiled into a single object). So go ahead, and add a new class to your project (the folder in which you create it is not important) and call it Product.vb. Replace the content of the generated class with the following code:

Option Strict On

'Required by the attributes
Imports System.ComponentModel
Imports System.ComponentModel.DataAnnotations

'The MetadataType is used to tell the class to get and get more information into another class
'Also note the Partial keyword
<MetadataType(GetType(ProductMetadata))> _
Partial Public Class Product

    'Add a business rules 
    Private Sub OnUnitsOnOrderChanging(ByVal value As System.Nullable(Of Short))
        If value + Me.UnitsInStock < Me.ReorderLevel Then
            Throw New ValidationException("Units on order cannot be less then reorder level")
        End If
    End Sub

End Class


'To add some behaviors to change the way things are displayed
Public Class ProductMetadata

    'Set a column name
    Private _ProdName As String
    <DisplayName("Product")> _
    Public Property ProductName() As String
        Get
            Return _ProdName
        End Get
        Set(ByVal value As String)
            _ProdName = value
        End Set
    End Property

    'Hide a column
    Private _discontinued As Boolean
    <ScaffoldColumn(False)> _
    Public Property Discontinued() As Boolean
        Get
            Return _discontinued
        End Get
        Set(ByVal value As Boolean)
            _discontinued = value
        End Set
    End Property

    'Set range validation
    Private _unitPrice As Decimal
    <Range(0.0, 100.0)> _
    Public Property UnitPrice() As Decimal
        Get
            Return _unitPrice
        End Get
        Set(ByVal value As Decimal)
            _unitPrice = value
        End Set
    End Property

    'Date format
    Private _CreationDate As Date
    <UIHint("MyDateTime")> _
    <DisplayFormat(DataFormatString:="{0:yyyy-MM-dd}")> _
    Public Property CreationDate() As Date
        Get
            Return _CreationDate
        End Get
        Set(ByVal value As Date)
            _CreationDate = value
        End Set
    End Property

End Class
Here are the things you need to focus on in this code snippet:
  • The Product class has been marked by the Partial keyword.
  • We are adding a MetadataType attribute to the Product class to tell the class to get more information from another class (named ProductMetadata).
  • This other class (ProductMetadata – the name is not important but by convention we use the name of the class to which we add the Metadata suffix) is simply a public class. This metadata class is where we customize the original model.
  • The DisplayName attribute (see the ProductName property) is used to replace the display name of the field. This name will be used anywhere the caption needs to be displayed.
  • The ScaffoldColumn attribute (see the Discontinued property) is used to hide a column. This column will be invisible for all the pages (grid or details). Be sure not to hide a mandatory field!
  • The Range attribute (see the UnitPrice property) is used to add a simple validation rule to the fields when you edit them.
  • The DisplayFormat attribute (see the CreationDate property) is used to provide a special display format. It has no effect on edit format (which will be affected by the UIHint attribute that will be covered later).
  • Finally, the OnUnitsOnOrderChanging method is in fact an event triggered by the data model that we override here to provide a special validation. Every columns of the table have the Changing and the Changed event that are triggered and that you can override with the code required by your application. You can open the .designer.vb file of your data model to see the signatures of those methods. Here, I have added a special cross validation that is impossible to do with the Range attribute.

You can now run the application and open the Products page. You will find all the enhancements we made to class but there is one that is not so obvious: the complex validation rule. This one is only effective you click the Update link.

Customising pages templates

You can customize any pages that are in the DynamicData/PageTemplates folder and those modifications will be applied to all the tables of your application.

If your goal to customize pages is to simply modify the name of some columns, go back to the previous section as it is the preferred solution. It will generate less code and then less code to maintain.

But what if you want to customize more the just caption for a single table? What if you want the grid to display a subset of the columns but having the details section display them all (the partial-ScaffoldColumn attribute does not exist)?

The answer is to customize the page to fit your requirements. In this example, we will customize the List page for the Products table.

The first thing we need to do here is to add a route to our Global.asax file because the Product table currently use the ListDetails.aspx page layout. Open the Global.asax and add this route just before the one for the Shippers table:

routes.Add(New DynamicDataRoute("Products/{action}.aspx") With { _
    .Constraints = New RouteValueDictionary(New With {.Action = "List|Details|Edit|Insert"}), _
    .Model = model, .Table = "Products"})
We now need to create the List.aspx page for the Products table. It is a lot easier to use the existing templates since most of the code is already in there.

Under the DynamicData/CustomPages folder, add a new folder that has the same name as the table in your database which is Products in our case (and not the name as exposed by your data model). If you are not sure, start your application and go to the page you want to customize, watch the URL. This is the name you need to use.

Open the PageTemplates folder, right-click the template you want to customize and select Copy from the contextual menu. Now, right-click the newly created folder and select Paste from the contextual menu.

If you have created a Web Site when you created the project, life is good. But if you selected a Web Application template, your Error List panel will now contains 18 errors (I warned you at the beginning) as shown in Figure 10!

Figure 10: 18 errors!

The reason behind this is the compilation method (or the lack of) used by the different models. In a Web Application, all your code-behinds files are compiled into a single DLL all the pages in ASP.Net are built with the Partial keyword. This keyword really helped us a couple of minutes ago extending an existing class but is now a nuisance! Web Site template does not have that kind of problems because it is not really compiled, or I should say not before the code is required and the path of the page is enough not to combine 2 classes having the same name but that are in different paths.

Fortunately fixing the errors is very easy (otherwise the old windows guy wouldn’t have found the solution by himself!). Ensure you click the “Show All Files” button of your Solution Explorer and modify these 3 things:

  1. Open the List.aspx.vb file in your Products folder and change the class name from List to a unique name like ListProducts. The Error List pane will now show 21 errors.
  2. Open the List.aspx.designer.vb file in your Products folder and change the class name for the same name you set in the previous step (ListProducts). The Error list will now be empty but it is not over yet!
  3. Finally, open the List.aspx file in your Products folder in source view and change the Inherits attributes of the Page directive (first line) for the same name you set before (ListProducts).
Now that errors have been fixed, we can start the customization. In this example, I will simply reduce the number of columns shown in the grid (leaving the number of fields in the Edit page untouched) and hide the Delete link.

Still in the List.aspx page of your Products folder, find the GridView control and add a property to stop the gridview from displaying all the columns automatically. This means that we now need to manually specify the columns. A new control named DynamicField is available and we must use it to dynamically display columns in a template style. The last thing to do for my example is to find the delete link and to remove it.

The resulting ASP.Net code for the GridView control now read like this:

<asp:GridView ID="GridView1" runat="server" DataSourceID="GridDataSource"
    AllowPaging="True" AllowSorting="True" CssClass="gridview"
    AutoGenerateColumns="false">
    <Columns>
    
        <asp:DynamicField DataField="ProductID" />
        <asp:DynamicField DataField="ProductName" />
        <asp:DynamicField DataField="UnitPrice" />
        <asp:DynamicField DataField="UnitsInStock" />
        <asp:DynamicField DataField="CreationDate" />
        
        <asp:TemplateField>
            <ItemTemplate>
                <asp:HyperLink ID="EditHyperLink" runat="server"
                    NavigateUrl='<%# table.GetActionPath(PageAction.Edit, GetDataItem()) %>'
                    Text="Edit" />
                 
                <asp:HyperLink ID="DetailsHyperLink" runat="server"
                    NavigateUrl='<%# table.GetActionPath(PageAction.Details, GetDataItem()) %>'
                    Text="Details" />
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>

    <PagerStyle CssClass="footer"/>        
    <PagerTemplate>
        <asp:GridViewPager runat="server" />
    </PagerTemplate>
    <EmptyDataTemplate>
        There are currently no items in this table.
    </EmptyDataTemplate>
</asp:GridView>
Now if you run the application again and open the Products table, you will see that the grid has fewer columns (with the Edit and Details links on the right) but that the customization made in the partial class are still applicable (column name and date format).

Customising field templates

You may have tried to edit a date field or a numeric field while testing and discovered that those fields are just a textbox. I agree that the validation rules won’t let you save invalid values but wouldn’t it be nice to provide a date time picker or a numeric textbox to the user? If you have not already bought a suite of controls like Telerik or Infragitics, have a look at eXcentris world which provides free components (source code is only available if you pay a minimal fee).

You can do it by customizing the field templates found under the DynamicData/FieldTemplates folder. Again, if you modify one of the templates directly, it will affect all the fields that are using it.

Instead of customizing an existing field template, say we want to create a new one. The process is a lot like creating a custom page but everything happens in the FieldTemplates folder. Right-click the DateTime_Edit.ascx file found in the FieldTemplates folder and select the Copy option from the contextual menu. Now, right-click the FieldTemplates folder and select the Paste option from the contextual menu. This operation will create a file called “Copy of DateTime_Edit.ascx”. Rename it to MyDateTime.ascx. If you have created a Web Application when you first created the project and if you take a look at the error list pane, you will discover the same kind of errors we had when we created the Product’s list page template. The very same problem of partial classes happens here. Do the same 3 modifications and your errors will magically disappear. For a real template, you should be ready to replace the textbox with a more convenient control. To keep this demo simple, add a back color and change the font of the textbox.

The final thing to do is to associate our fields with this new template. If we would have modified the original template, all the fields of this data type would have used our modified template but we have created a new field template with a new name. We already did that association. Re-open the Product.vb class you created earlier and take a look at the CreationDate property. We have already set the UIHint attribute to MyDateTime. This is where the magic occurs.

But wait a minute, we named our control MyDateTime_Edit. There is a convention in the field templates that says that a control without the _Edit is to display the value and the one with _Edit is for ... edition! We haven’t created the one without the suffix and it is not really important as long as we don’t want to modify the display behaviour.

If you run the application now, you will see that the CreationDate field will have a new format (yellow background and italic in my demo). This new field template is used whatever route (List or ListDetails) is used.

Hiding a table

There are times when you have a table in your data model but you would want to hide from the list of tables from the default page.

You could re-open the Global.asax file, change the ScaffoldAllTables property to False and add a route for every table of your model but that would require creating custom pages for all the tables (and I don’t know the logic behind this).

The other way, which is a lot easier, is to add another partial class and qualify this class with the ScaffoldTable attribute as shown here:

Option Strict On

'Required by the attributes
Imports System.ComponentModel
Imports System.ComponentModel.DataAnnotations

<ScaffoldTable(False)> _
Partial Public Class Region
End Class
Adding this short little class will hide the Region table from the list of tables of the first page but will still render it in other pages like the Territories page.

Video demonstration of all this on MyVBProf.com

Now that you have read this article entirely, I can tell you that my friend Bill Burrows, another VB MVP, offers a series of 12 videos showing you about the same thing I have demonstrated here. You can watch those excellent videos from his web site.

Conclusion

I don’t think that all web applications can get real benefits from this technology. But just about every application that are data-driven have lookup/reference tables which are inaccessible from the users because there is no time left in the project to create maintenance pages for those tables. Now you have no excuses!

Too bad such a feature does not exist for Windows application!


(Print this page)