A Dynamic Menu For Your Dynamic Data

Posted on August 27, 2008 by Matt Berseth.
Categories: ASP.NET, Contributors, Dynamic HTML.

So I am still playing around with building a Northwind Dynamic Data web site. Tonight I thought it would be interesting to see what it would take to create a menu for navigating the tables in the site. I was particularly interested in seeing if I could get some grouping or categorization to the metadata so I could create a multi-leveled menu. It turns out it wasn't too difficult at all (see the screen shot below - the menu is on the left). I have my tables organized into 4 categories: Sales, People, Products and Reports. And the cool thing is that this menu is completely dynamic. You can add, remove or reorganize the categories without touching the UI. And depending where you are keeping your metadata you could even do this without recompiling your app. The grouping is automatically discovered from the metadata and the menu is built solely off the it so everything 'just works'.

Besides adding the grouping information, I also tagged each of my tables with a custom description that I am displaying under the grids title. Nothing too complicated, but still interesting. Read on if you are curious how I did this and don't forget to check out the download.

Download

image

Adding Category and Description

When MetaTable objects are created, the Dynamic Data components automatically populate the the MetaTable's DisplayName property from the DisplayNameAttribute that is hanging off the metadata class (if you are using the default metadata provider). This is why you see the nice 'Products By Category' title in the screen shot above. I have specifically told Dynamic Data to use this value because I tagged my metadata class with the DisplayName attribute and given it a value of 'Products By Category'. Below this metadata ...

  1. [MetadataType(typeof(ProductByCategoryMetadata))]
  2. public partial class Products_by_Category { }
  3.  
  4. [DisplayName("Products By Category")]
  5. public class ProductByCategoryMetadata
  6. {
  7.     [ScaffoldColumn(false)]
  8.     public object Discontinued { get; set; }
  9. }

To add a category and description to the metadata, I just used the existing Category and Description attributes and added them to the metadata class as well. So now we have ...

  1. [MetadataType(typeof(ProductByCategoryMetadata))]
  2. public partial class Products_by_Category { }
  3.  
  4. [Category("Products")]
  5. [Description("You can use this page to view your products by category")]
  6. [DisplayName("Products By Category")]
  7. public class ProductByCategoryMetadata
  8. {
  9.     [ScaffoldColumn(false)]
  10.     public object Discontinued { get; set; }
  11. }

The Category and Description attributes don't directly map to any properties on the MetaTable type. But, any extra custom attributes that are applied to in the metadata are passed through to the Attribute collection that hangs off the MetaTable class. So with a couple of pretty simple extensions methods I can add them myself (ignoring error handling for now) ...

  1. public static class MetaTableExtensions
  2. {
  3.     /// <summary>
  4.     /// Gets the description for the MetaTable
  5.     /// </summary>
  6.     public static string GetDescription(this MetaTable table)
  7.     {
  8.         return ((DescriptionAttribute)table.Attributes[typeof(DescriptionAttribute)]).Description;
  9.     }
  10.  
  11.     /// <summary>
  12.     /// Gets the category for the MetaTable
  13.     /// </summary>
  14.     public static string GetCategory(this MetaTable table)
  15.     {
  16.         return ((CategoryAttribute)table.Attributes[typeof(CategoryAttribute)]).Category;
  17.     }
  18. }

... and now to get at the MetaTable's description or category I can just go through these methods. So I updated the List template and added a little bit of code that generates a simple title bar generated from the MetaTables DisplayName and Description attributes.

image

and now our List pages have a nice dynamic title bar ...

image

Building the Menu

To build the menu, I am using a ListView tied to a LinqDataSource that uses a Linq query to create a 2 level object structure that I can bind to. First, I wired the LinqDataSource's Selecting event to the following bit of code that groups my tables by their category ...

  1. protected void LdsMenu_Selecting(object sender, LinqDataSourceSelectEventArgs e)
  2. {
  3.     e.Result =
  4.         from vt in MetaModel.Default.VisibleTables
  5.         //  use the category to group the tables
  6.         group vt by vt.GetCategory() into groups
  7.         select new
  8.         {
  9.             CategoryName = groups.Key,
  10.             Tables = groups
  11.         };
  12. }

Then I bound this data source to my ListView ...

image

And that's all it took to build my 2 level menu. Awesome!

image

Conclusion

Can Dynamic Data be used for more than admin screens and prototyping? I think it might. What about you?

That's it. Enjoy!

How to respond with code 404 (Not Found) in ASP.NET

Posted on August 26, 2008 by (author unknown).
Categories: Contributors, Dynamic HTML.
Suppose you configured custom 404 page in web.config file in the customErrors section. So whenever user requests non-existent aspx page, ASP.NET run-time returns well formatted message to the user. Also you have a page that shows articles from a database according to article ID passed in the url (for example: article.aspx?id=345). But if user passes article ID that doesn't exists the page must return code 404 (Not Found) and show the custom 404 page like in the previous situation. Fortunately you don't need to parse the customErrors section to get name of the custom 404 page. Just throw HttpException:
throw new HttpException(404, "Article not found");
ASP.NET run-time will catch the exception and will redirect to the custom 404.

Dynamic Data and Custom Metadata Providers

Posted on August 24, 2008 by Matt Berseth.
Categories: ASP.NET, Contributors.

In my previous post on Dynamic Data, I mentioned that you can use the MetadataType attribute to point Dynamic Data at class that contains additional metadata for your model. This additional metadata will give you more control over how your UI elements render. If you don't want a column to display in your GridView, want to change the column header text from EmployeeID to Employee ID or want the cell values formatted a little differently this metadata class is where this information gets specified. The code snippet below shows how this class can be used for customization.

Download

  1. //  Attach the Employee Metadata to the Employee
  2. //  entity that the LINQ to SQL designer generates
  3. [MetadataType(typeof(EmployeeMetadata))]
  4. public partial class Employee
  5. {
  6. }
  7.  
  8. //  Attach some additional metadata
  9. public class EmployeeMetadata
  10. {
  11.     //  Rename the EmployeeID column to Employee ID
  12.     [DisplayName("Employee ID")]
  13.     public object EmployeeID { get; set; }
  14.  
  15.     //  Format the Hire Date
  16.     [DisplayFormat(DataFormatString = "{0:d}")]
  17.     public object HireDate { get; set; }
  18.  
  19.     //  Hide the HomePhone column
  20.     [ScaffoldColumn(false)]
  21.     public object HomePhone { get; set; }
  22. }

That is pretty cool. And what's even better is that if you don't like storing this information as attributes, you can swap out the default implementation and replace it with a solution that better fits your needs. Stuff your metadata in an XML file, flat file, in-memory, or database - it is pretty much up to you. All you need to do is write the TypeDescriptor logic that rebuilds the metadata from where ever it is you have placed it.

Below shows three different ways of specifying the a MetadataProviderFactory. Internally, ContextConfiguration uses the AssociatedMetadataTypeTypeDescriptionProvider if a custom factory is not provided so the first two calls to RegisterContext do exactly the same thing. In the third example I have provided my own custom provider, XmlMetadataDescriptionProvider, that reads the metadata from an xml file.

  1. //  Example 1:
  2. //  just use the default metadata provider
  3. model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
  4. {
  5.     ScaffoldAllTables = true
  6. });
  7.  
  8. //  Example 2:
  9. //  this is exactly the same as above       
  10. model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
  11. {
  12.     ScaffoldAllTables = true,
  13.     MetadataProviderFactory = (type => new AssociatedMetadataTypeTypeDescriptionProvider(type))
  14. });               
  15.  
  16. //  Example 3:
  17. //  here I am using a custom provider that reads the metadata from
  18. //  an xml file
  19. model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
  20. {
  21.     ScaffoldAllTables = true,
  22.     MetadataProviderFactory = (type => new XmlMetadataDescriptionProvider(type, "metadata.xml"))
  23. });

Implementing a Custom Metadata Provider

While building my Simple 5 Table Dynamic Data Northwind example last week, I found myself typing in the same type of metadata information for each of the properties I was showing on my screens. I was adding attributes for things like ...

  • Add spaces into the column headers. So ShippedDate would become Shipped Date.
  • Stripping the time component from my DateTime properties
  • Formatting my decimal properties as currency

So I created a new TypeDescriptionProvider that I have configured to supplement the AssociatedMetadataTypeTypeDescriptionProvider with additional metadata that is generated by a handful of rules. Stuff like ...

  • DateTime properties should have a default format of {0:d}
  • decimal properties should have a default format of {0:c}
  • Split the property name into its word components and use that as its display name

It turns out there isn't a whole lot to my custom provider (download the code and take a peek). I just run a piece of code that checks to see if the property already has the DisplayName and DisplayFormat attributes defined. If so its a no-op. If not, I use some simple rules to generate these attributes and add them to the PropertyDescriptor. Below is the core logic. A few things to note ...

  • Line 12: I first check to see if the property already has the DisplayNameAttribute defined. If it does I don't do anything. But if it doesn't have this attribute defined, I use the properties name to generate the friendly display name using the ToHumanFromPascal function (which I stole from here).
  • Line 24: I do the same here. If the property doesn't have the DisplayFormatAttribute I get the default display format for the property type and apply that.
  1. public override PropertyDescriptorCollection GetProperties()
  2. {
  3.     List<PropertyDescriptor> propertyDescriptors = new List<PropertyDescriptor>();
  4.  
  5.     foreach (PropertyDescriptor propDescriptor in base.GetProperties())
  6.     {
  7.         List<Attribute> newAttributes = new List<Attribute>();
  8.  
  9.         //  Display Name Rules ...
  10.         //  If the property doesn't already have a DisplayNameAttribute defined
  11.         //  go ahead and auto-generate one based on the property name
  12.         if (!HasAttribute<DisplayNameAttribute>(propDescriptor))
  13.         {
  14.             //  generate the display name
  15.             string friendlyDisplayName = ToHumanFromPascal(propDescriptor.Name);
  16.  
  17.             //  add it to the list
  18.             newAttributes.Add(new DisplayNameAttribute(friendlyDisplayName));
  19.         }
  20.  
  21.         //  Display Format Rules ...
  22.         //  If the property doesn't already have a DisplayFormatAttribute defined
  23.         //  go ahead and auto-generate one based on the property type
  24.         if (!HasAttribute<DisplayFormatAttribute>(propDescriptor))
  25.         {
  26.             //  get the default format for the property type
  27.             string displayFormat = GetDisplayFormat(propDescriptor.PropertyType);
  28.  
  29.             //  add it to the list
  30.             newAttributes.Add(new DisplayFormatAttribute() { DataFormatString = displayFormat });
  31.         }
  32.  
  33.         propertyDescriptors.Add(new WrappedPropertyDescriptor(propDescriptor, newAttributes.ToArray()));
  34.     }
  35.  
  36.     //  return the descriptor collection
  37.     return new PropertyDescriptorCollection(propertyDescriptors.ToArray(), true);
  38. }

So what does all of this produce? Well, with this metadata ...

  1. //  Attach the OrderMetadata to the Order class
  2. [MetadataType(typeof(OrderMetadata))]
  3. public partial class Order {}
  4.  
  5. [TableName("My Orders")]
  6. public class OrderMetadata
  7. {
  8.     //  Columns I want hidden
  9.     [ScaffoldColumn(false)]
  10.     public object RequiredDate { get; set; }
  11.     [ScaffoldColumn(false)]
  12.     public object ShipVia { get; set; }
  13.     [ScaffoldColumn(false)]
  14.     public object Freight { get; set; }
  15.     [ScaffoldColumn(false)]
  16.     public object ShipName { get; set; }
  17.     [ScaffoldColumn(false)]
  18.     public object ShipPostalCode { get; set; }
  19.     [ScaffoldColumn(false)]
  20.     public object ShipCountry { get; set; }
  21. }

and this configuration ...

  1. model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration() {
  2.     ScaffoldAllTables = true
  3. });

the orders grid looks like this. Notice the concatenated column headers and the OrderDate and ShippedDate cell values ...

image

but with the same metadata and my custom metadata provider ...

  1. model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
  2. {
  3.     ScaffoldAllTables = true,
  4.     MetadataProviderFactory = (type => new DefaultTypeDescriptionProvider(type, new AssociatedMetadataTypeTypeDescriptionProvider(type)))
  5. });

it looks like this ...

image

Conclusion

I am sure a few people are wincing that I am applying these rules at run-time when they are statically known. No problem, move these rules from the TypeDescriptor and into your build process and auto-generate the metadata class or move the stuff to an xml file and write your own custom provider. Or you can even use a hybrid approach like I have done here that supplements the default attribute implementation with a few basic rules which are evaluated at run-time. The cool thing here is that you can choose what best fits your needs.

That's it. Enjoy!

Chain.js - data binding for jQuery

Posted on August 22, 2008 by Jason Striegel.
Categories: Contributors, Dynamic HTML.

chain_js_20080821.jpg

Rizqi Ahmad, a high-school student in Germany, created a pretty useful data binding service for jQuery called Chain.js. It allows you to easily manipulate data driven content from javascript by directly manipulating the DOM, without resorting to templates or a lot of complicated code. You create the markup the way you want the data to display, give class names to DOM elements that should have their content substituted, and pass an associative array containing the variable data to the chain() method.

There is also support for managing lists of elements, allowing you to add and remove elements dynamically inside the defined markup. Rizqi also created the Interaction library that works on top of the data binding library to provide drag, drop, and sort support for lists.

Make sure to check out his demos. They show off some of the flexibility of the library and they're easy to tweak for your own needs.

Data Binding Solution for jQuery
Chain.js - Data Binding Service for jQuery
Interaction.js - drag/drop/sort support for Chain.js

ASP.NET Dynamic Data - Simple 5 Table Northwind Example

Posted on August 21, 2008 by Matt Berseth.
Categories: ASP.NET, Contributors.
I have been anxiously awaiting the Dynamic Data release. And now that it is here (it was released with VS 2008 and .Net 3.5 SP1) I decided I would start getting a feel for what is has to offer by building a real simple Dynamic Data web site that allows you to browse the 5 core Northwind tables - Customers, Employees, Orders, Products and Suppliers. Read on for the details and don't forget to download the code. DiscountASP hasn't quite upgraded to SP1 so I don't have a live demo setup. Hopefully they will get the upgrade completed soon, but I made sure to include lots of screen shots so you can get a good idea of what the screens look like. Download image

What is Dynamic Data?

I try to keep a close eye on ASP.NET, but I didn't know Dynamic Data was shipping as part of SP1. I asked around the office and found out this took a few other people by surprise too. And more than a few people had actually never heard of Dynamic Data. In case you fall into that category, here are a couple of quotes that describe what DynamicData is all about. From Wikipedia ...
ASP.NET Dynamic Data is a web application scaffolding framework from Microsoft, shipped as an extension to ASP.NET, that can be used to build data driven web applications. It exposes tables in a database by encoding it in the URI of the ASP.NET web service, and the data in the table is automatically rendered to HTML. The process of rendering can be controlled using custom design templates. Internally, it discovers the database schema by using the database metadata.
From asp.net ...
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.
So for this demo app, I am planning on using Dynamic Data to build a web application that allows me to browse the Northwind database.

Create the Dynamic Data Web Site

To get started building a Dynamic Data Web Site, you do the usual File -> New -> Web Site and select either the Dynamic Data Entities Web Site or Dynamic Data Web Site templates. If you plan on using the ADO.NET Entity Framework for data access you can select the first option, otherwise if you are using LINQ to SQL you select the second template. For this example I am using LINQ to SQL so I chose the Dynamic Data Web Site template. image When VS finishes loading, you will notice a number of files/folders have been included in the solution. The solution includes a DynamicData folder that is filled with other folders and each is filled with both UserControls and regular ASP.NET Pages. image

Create and Register the LINQ to SQL DataContext

I am going to use LINQ to SQL to access the my Northwind data, so I used the VS designer create the LINQ to SQL classes for the tables I want by dragging my tables over from the server explorer and dropping them onto the design surface. image After creating the DataContext, I update the Global.asax and register my NorthwindDataContext with the DynamicData system. The web site template generates the code that handles all of this, you just have to read the comments and plug in your custom context type. Here is what it ends up looking like ...
  1. public static void RegisterRoutes(RouteCollection routes)
  2. {
  3.   MetaModel model = new MetaModel();
  4.   // IMPORTANT: DATA MODEL REGISTRATION
  5.   // Uncomment this line to register LINQ to SQL classes or an ADO.NET Entity Data
  6.   // model for ASP.NET Dynamic Data. Set ScaffoldAllTables = true only if you are sure
  7.   // that you want all tables in the data model to support a scaffold (i.e. templates)
  8.   // view. To control scaffolding for individual tables, create a partial class for
  9.   // the table and apply the [Scaffold(true)] attribute to the partial class.
  10.   // Note: Make sure that you change "YourDataContextType" to the name of the data context
  11.   // class in your application.
  12.   model.RegisterContext( typeof(NorthwindDataContext), new ContextConfiguration() { ScaffoldAllTables = true });
  13.  
  14.   // The following statement supports separate-page mode, where the List, Detail, Insert, and
  15.   // Update tasks are performed by using separate pages. To enable this mode, uncomment the following
  16.   // route definition, and comment out the route definitions in the combined-page mode section that follows.
  17.   routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
  18.     Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert"}), Model = model
  19.   });
  20.  
  21.   // The following statements support combined-page mode, where the List, Detail, Insert, and
  22.   // Update tasks are performed by using the same page. To enable this mode, uncomment the
  23.   // following routes and comment out the route definition in the separate-page mode section above.
  24.   //routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx") {
  25.   // Action = PageAction.List,
  26.   // ViewName = "ListDetails",
  27.   // Model = model
  28.   //});
  29.  
  30.   //routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx") {
  31.   // Action = PageAction.Details,
  32.   // ViewName = "ListDetails",
  33.   // Model = model
  34.   //});
  35.  
  36.  }
And really after you have done this you have a pretty nice looking and functional web site. A landing page that lets you know about what tables are available ... image Grids for viewing the contents of these tables (paging and sorting included) ... image And pages for Inserting, Updating and viewing Details ... image

Customization

If I was happy with how all of the default pages look, I could have stopped here and had a functional web application that supports basic CRUD operations. That's pretty cool. And that took all of what ... 5 minutes? But what if I would like to change a few things?
Customizing the List Page Template
The List template is located in the DynamicData/PageTemplates folder. The default List page look pretty good, but I wanted to see what it would take to make a few changes. So I opened the List.aspx page and started making my changes.
  • First, I added a black border by wrapping the GrdView in a series of DIVs that use background images to style the border.
  • Next, I increased the PageSize from 10 to 15.
  • Then added a different Data Pager
  • Finally, I want my site to be read-only so I removed the Insert, Edit and Delete links
Nothing too major, but styles provide quite a bit of change. image
Customizing the Details Page Template
Next, I made similar changes to the Details page template. image
Adding Custom Metadata to the Model
There are a few things that you will notice in the screen shots above with my custom pages that are different from the default pages.
  • The grids column counts are different (my custom pages have fewer columns)
  • Some of the column headers have different names
  • Some of the grid's cell values are formatted differently
  • The display name for the table
To customize these items, you need to create a metadata class that provides Dynamic Data with a little more information about your entities. I will be honest, this part seems a little weird to me, but that might just be because it is new. Anyway, to accomplish this you need to create two more classes for each of the classes in your DataContext. So for my 5 table sample, I need to create 10 more classes to attach my metadata. First you add a partial class with the same name of the entity class in the data model and then apply an attribute to this partial class that ties it to the additional metadata we have defined for the class. The code snippet below is what does this for the Order entity in our sample application. The following bit of code tells Dynamic Data the following ...
  • The display name for the Order table is 'My Orders' - Applied with the TableName attributes
  • The OrderDate, ShippedDate, ShipAddress and ShipCity columns all have overridden display names - Applied with the DisplayName attribute
  • The OrderDate and ShippedDate columns have a custom format - Applied with the DisplayFormat attribute
  • The RequiredDate, ShipVia, Freight, ShipName, ShipPostalCode, and ShipCountry columns should be hidden from the UI - Applied with the ScaffoldColumn attribute
  1. // Attach the OrderMetadata to the Order class
  2. [MetadataType(typeof(OrderMetadata))]
  3. public partial class Order {}
  4.  
  5. [TableName("My Orders")]
  6. public class OrderMetadata
  7. {
  8.   // Override the display name
  9.   [DisplayName("Date Ordered")]
  10.  
  11.   // Format the Date
  12.   [DisplayFormat(DataFormatString="{0:d}")]
  13.   public object OrderDate { get; set; }
  14.  
  15.   // Override the display name
  16.   [DisplayName("Date Shipped")]
  17.  
  18.   // Format the Date
  19.   [DisplayFormat(DataFormatString = "{0:d}")]
  20.   public object ShippedDate { get; set; }
  21.  
  22.   // Override the display name
  23.   [DisplayName("Address")]
  24.   public object ShipAddress { get; set; }
  25.  
  26.   // Override the display name
  27.   [DisplayName("City")]
  28.   public object ShipCity { get; set; }
  29.  
  30.   // Columns I want hidden
  31.   [ScaffoldColumn(false)]
  32.   public object RequiredDate { get; set; }
  33.  
  34.   [ScaffoldColumn(false)]
  35.   public object ShipVia { get; set; }
  36.  
  37.   [ScaffoldColumn(false)]
  38.   public object Freight { get; set; }
  39.  
  40.   [ScaffoldColumn(false)]
  41.   public object ShipName { get; set; }
  42.  
  43.   [ScaffoldColumn(false)]
  44.   public object ShipPostalCode { get; set; }
  45.  
  46.   [ScaffoldColumn(false)]
  47.   public object ShipCountry { get; set; }
  48. }

Conclusion

Well that was my first pass through building a Dynamic Data site. I am pretty impressed and I am looking forward to exploring this further - stay tuned. That's it. Enjoy!