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!

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!

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!

Easily build powerful client-side AJAX paging, using jQuery

Posted on August 20, 2008 by Dave Ward.
Categories: ASP.NET, Contributors.

A book's pages blowing in the wind

When I suggest that developers consider using web services and a more client-centric approach to solve their UpdatePanel performance problems, the lack of paging is often their first objection.

Conventional ASP.NET wisdom seems to hold that the GridView/UpdatePanel combo is king when asynchronously paging through a data source. If you’ll give me a few minutes of your time, I’d like to challenge that notion!

In this post, I’m going to show you how to implement great client-side paging, using jQuery and ASP.NET AJAX. I’ll be building on foundation laid in two previous posts: Use jQuery and ASP.NET AJAX to build a client side Repeater and How to easily enhance your existing tables with simple CSS. If you haven’t yet, I recommend that you read those posts first, so that we’re all on the same page.

This post will be a bit longer than those were, but I’ve tried to divide the ordeal into granular steps that break the monotony as much as possible:

  • Setting up a page method to retrieve individual pages of data.
  • Using jQuery to consume that page method and render its result.
  • Adding progress indication during the initial load.
  • Adding the basic HTML paging controls.
  • Wiring up event handlers for the paging controls.
  • Implementing the paging functions that those handlers call.
  • Adding progress indication during the paging operations.
  • Using a page method to dynamically determine the size of the data source.

Retrieving a particular “page” of data

Our existing GetFeedBurnerItems page method accepts a PageSize parameter, which specifies how many items it should return. To implement paging, we need to add a new parameter: Page.

This second argument will specify which page items will be returned from.

  1. [WebMethod]
  2. public static IEnumerable GetFeedburnerItems(int PageSize, int Page)
  3. {
  4.   XDocument feedXML =
  5.     XDocument.Load(&quot;http://feeds.encosia.com/Encosia&quot;);
  6.  
  7.   var feeds =
  8.     from feed in feedXML.Descendants(&quot;item&quot;)
  9.     select new
  10.     {
  11.       Date = DateTime.Parse(feed.Element(&quot;pubDate&quot;).Value)
  12.                      .ToShortDateString(),
  13.       Title = feed.Element(&quot;title&quot;).Value,
  14.       Link = feed.Element(&quot;link&quot;).Value,
  15.       Description = feed.Element(&quot;description&quot;).Value,
  16.     };
  17.  
  18.   return feeds.Skip((Page - 1) * PageSize).Take(PageSize);
  19. }

Using the supplied Page argument, our service uses IQueryable’s Skip() extension method to return the correct page of results.

The purpose of the subtraction is to make the Page argument 1-based. I don’t think the notion of a “page 0? makes much sense in this scenario, so I prefer they start at 1.

Consuming the service and rendering the result

Now that our service is modified to provide paged results, we can consume the first page of results similar to how we did in the previous examples.

  1. // Set this to any integer you like. 5-7 works well
  2. //  with the FeedBurner data source.
  3. var pageSize = 5;
  4.  
  5. $(document).ready(function() {
  6.   // Display the first page of results initially.
  7.   DisplayRSSTable(1);
  8. });
  9.  
  10. function DisplayRSSTable(page) {
  11.   $.ajax({
  12.     type: &quot;POST&quot;,
  13.     url: &quot;Default.aspx/GetFeedBurnerItems&quot;,
  14.     data: &quot;{&#39;PageSize&#39;:&#39;&quot; + pageSize + &quot;&#39;, &#39;Page&#39;:&#39;&quot; + page + &quot;&#39;}&quot;,
  15.     contentType: &quot;application/json; charset=utf-8&quot;,
  16.     dataType: &quot;json&quot;,
  17.     success: function(msg) {
  18.       // Render the resulting data, via template.
  19.       ApplyTemplate(msg);
  20.     }
  21.   });
  22. }
  23.  
  24. function ApplyTemplate(msg) {
  25.   // Changed the template extension from .tpl to .htm,
  26.   //  to avoid the request being blocked by some IIS installs.
  27.   $('#Container').setTemplateURL('RSSTable.htm',
  28.                                  null, { filter_data: false });
  29.   $('#Container').processTemplate(msg);
  30. }

Since we’ll be calling the page method from other parts of the code, it made sense to refactor that functionality into a separate function: DisplayRSSTable.

Other than that, this is basically the same code that you’ve seen in the previous jQuery templating examples.

Adding initial progress indication on page load

FeedBurner is a great service, but tends to be a bit slow. Due to that latency on the response, the initial page often takes a few seconds to render. This has prompted several of you to ask about adding progress indication, which is a great idea.

There are many perfectly legitimate ways of accomplishing this, but I want to focus on minimizing the harshness of the transition between progress indication and content display. Jumping from a simple loading indicator to a full table is jarring.

One way to smooth that transition is to use a structure very similar to the final content’s template:

  1. &lt;div id=&quot;Container&quot;&gt;
  2.   &lt;table id=&quot;RSSTable&quot; cellspacing=&quot;0&quot;&gt;
  3.     &lt;thead&gt;
  4.       &lt;tr&gt;
  5.         &lt;th&gt;&lt;/th&gt;
  6.       &lt;/tr&gt;
  7.     &lt;/thead&gt;
  8.     &lt;tbody&gt;
  9.       &lt;td class=&quot;loading&quot;&gt;&lt;/td&gt;
  10.     &lt;/tbody&gt;
  11.   &lt;/table&gt;
  12. &lt;/div&gt;

The .loading CSS class is defined as follows:

  1. table tbody .loading {
  2.   /* Since the table may be empty, set a decent default height. */
  3.   height: 350px;
  4.  
  5.   /* Center an animated progress indicator over the table body. */
  6.   background-image: url(images/progress-indicator.gif);
  7.   background-position: center center;
  8.   background-repeat: no-repeat;
  9. }

The end result is an empty table, with a centered progress indicator inside its single 350px tall body cell.

Adding paging controls to the HTML template

Now that the first page of results is coming through smoothly, we need to add a paging interface to get at the rest of the data. To keep this from running longer than a Steve Yegge post, let’s stick with a simple previous/next system.

Since it’s logically a part of the rendered template, I decided to add the paging controls to the template itself:

  1. &lt;table id=&quot;RSSTable&quot; cellspacing=&quot;0&quot;&gt;
  2.   &lt;thead&gt;
  3.     &lt;tr&gt;
  4.       &lt;th width=&quot;80&quot;&gt;Date&lt;/th&gt;
  5.       &lt;th&gt;Title / Excerpt&lt;/th&gt;
  6.     &lt;/tr&gt;
  7.   &lt;/thead&gt; 
  8.   &lt;tbody&gt;
  9.     {#foreach $T.d as post}
  10.     &lt;tr&gt;
  11.       &lt;td rowspan=&quot;2&quot;&gt;{$T.post.Date}&lt;/td&gt;
  12.       &lt;td&gt;&lt;a href=&quot;{$T.post.Link}&quot;&gt;{$T.post.Title}&lt;/a&gt;&lt;/td&gt;
  13.     &lt;/tr&gt;
  14.     &lt;tr&gt;
  15.       &lt;td&gt;{$T.post.Description}&lt;/td&gt;
  16.     &lt;/tr&gt;
  17.     {#/for}
  18.   &lt;/tbody&gt;
  19. &lt;/table&gt;
  20.  
  21. &lt;a id=&quot;PrevPage&quot; class=&quot;paging&quot;&gt;Previous Page&lt;/a&gt;
  22. &lt;a id=&quot;NextPage&quot; class=&quot;paging&quot;&gt;Next Page&lt;/a&gt;

This template is very similar to the template that was used in the past two posts. The important changes are:

  • The post title and excerpt are rearranged to be vertically oriented.
  • The date column is fixed width, to maintain consistent column widths.
  • Of course, the paging controls are added to the bottom of the table.

After implementing these changes, our rendered table is shaping up nicely:

An example of the rendered template

Initializing the paging interface elements

Now that we’ve got the template rendering those anchor tags, the next step is to add functionality to make them do something:

  1. // Initialize this and store globally for tracking state.
  2. var currentPage = 1;
  3.  
  4. // The feed has 15 items, and we're displaying 5 per page.
  5. var lastPage = 3;
  6.  
  7. function UpdatePaging() {
  8.   // If we&#39;re not on the first page, enable the &quot;Previous&quot; link.
  9.   if (currentPage != 1) {
  10.     $('#PrevPage').attr('href', '#');
  11.     $('#PrevPage').click(PrevPage);
  12.   }
  13.  
  14.   // If we&#39;re not on the last page, enable the &quot;Next&quot; link.
  15.   if (currentPage != lastPage) {
  16.     $('#NextPage').attr('href', '#');
  17.     $('#NextPage').click(NextPage);
  18.   }
  19. }

UpdatePaging adds an href attribute of # to one or both of the paging anchor tags, thereby making them active links. This is primarily to give your users a good UI hint that they can click the links. The anchor’s actual navigation functionality won’t be needed.

.click() is jQuery’s way of attaching click handlers to elements. In this case, we’re wiring up a couple of functions that will handle the actual task of changing pages.

Putting it together, DisplayRSSTable needs to be updated to call UpdatePaging immediately after every template rendering:

  1. function DisplayRSSTable(page) {
  2.   $.ajax({
  3.     type: &quot;POST&quot;,
  4.     url: &quot;Default.aspx/GetFeedBurnerItems&quot;,
  5.     data: &quot;{&#39;PageSize&#39;:&#39;&quot; + pageSize + &quot;&#39;, &#39;Page&#39;:&#39;&quot; + page + &quot;&#39;}&quot;,
  6.     contentType: &quot;application/json; charset=utf-8&quot;,
  7.     dataType: &quot;json&quot;,
  8.     success: function(msg) {
  9.       // Render the resulting data, via template.
  10.       ApplyTemplate(msg);
  11.  
  12.       // Wireup appropriate paging functionality.
  13.       UpdatePaging();
  14.     }
  15.   });
  16. }

By doing this, we can be sure that the paging functionality always reflects the currently rendered page.

Making it all work: implementing the paging functions

Finally, it’s time to implement the PrevPage and NextPage functions that are responsible for actually changing pages.

This is where you really appreciate that the navigation links are generated as part of the template. That allows us to fire off a DisplayRSSTable and not worry about updating the navigation click handlers. Those updates to the paging controls will be automatically handled by the UpdatePaging after the requested page’s template renders.

  1. function NextPage(evt) {
  2.   // Prevent the browser from navigating needlessly to #.
  3.   evt.preventDefault();
  4.  
  5.   // Entertain the user while the previous page is loaded.
  6.   DisplayProgressIndication();
  7.  
  8.   // Load and render the next page of results, and
  9.   //  increment the current page number.
  10.   DisplayRSSTable(++currentPage);
  11. }
  12.  
  13. function PrevPage(evt) {
  14.   // Prevent the browser from navigating needlessly to #.
  15.   evt.preventDefault();
  16.  
  17.   // Entertain the user while the previous page is loaded.
  18.   DisplayProgressIndication();
  19.  
  20.   // Load and render the previous page of results, and
  21.   //  decrement the current page number.
  22.   DisplayRSSTable(--currentPage);
  23. }

The call to preventDefault cancels the browser’s navigation to #. It doesn’t interfere with the paging functionality to allow that navigation, but allowing it will unnecessarily send mixed signals to your users.

Using the currentPage global variable, we can use our original DisplayRSSTable function to request one page up or down from the current page.

Progress indication during paging requests

Since each page request takes a couple seconds to complete, it’s important to provide progress indication during that time. This can be implemented in the same way as the initial page load progress indication. We’ll even reuse the same .loading CSS class.

  1. function DisplayProgressIndication() {
  2.   // Hide both of the paging controls,
  3.   //  to avoid click-happy users.
  4.   $('.paging').hide();
  5.  
  6.   // Clean up our event handlers, to avoid memory leaks.
  7.   $('.paging').unbind();
  8.  
  9.   // Store the height of the content area of the table.
  10.   var height = $('#RSSTable tbody').height();
  11.  
  12.   // Replace the entire content area with a single row/cell.
  13.   $('#RSSTable tbody').html(&#39;&lt;tr&gt;&lt;td colspan=&quot;2&quot;&gt;&lt;/td&gt;&lt;/tr&gt;&#39;);
  14.  
  15.   // Set that row's height to be the same as previous.
  16.   $('#RSSTable tbody tr').height(height);
  17.  
  18.   // Add our centered progress indicator animation to it.
  19.   $('#RSSTable tbody td').addClass('loading');
  20. }

This function does the following:

  • Hides the paging controls and clears any event handlers on them.
  • Determines the current height of the table’s content area.
  • Drops the entire content area and replaces it with a single cell.
  • Sets that cell’s containing row’s height to the previously determined value.
  • Add the .loading class to that cell, so that a progress animation appears centered over the same areas that the content used to occupy.

Dynamically determine the size of our data

So far, we’ve been working under the assumption that there will always be 15 elements in the data source and that it will never change at run time. While this might be a workable assumption for my RSS feed, it certainly would not be for most other data sources.

We need to dynamically detect the size of our data.

The first step in implementing this functionality is to interrogate our data source on the server side. This page method will return the total number of items in the feed:

  1. [WebMethod]
  2. public static int GetFeedBurnerItemCount()
  3. {
  4.   XDocument feedXML =
  5.     XDocument.Load(&quot;http://feeds.encosia.com/Encosia&quot;);
  6.  
  7.   return feedXML.Descendants(&quot;item&quot;).Count();
  8. }

Using the $.ajax() syntax that I have previously described, we can request the count of data items from our new page method. Using that and the global pageSize variable, it’s easy to determine the lastPage:

  1. // Initialize this to 1, so that &quot;Next&quot; is disabled until
  2. //  GetItemCount returns and we know there's a second page.
  3. var lastPage = 1;
  4.  
  5. function GetRSSItemCount() {
  6.   $.ajax({
  7.     type: &quot;POST&quot;,
  8.     url: &quot;Default.aspx/GetFeedBurnerItemCount&quot;,
  9.     data: &quot;{}&quot;,
  10.     contentType: &quot;application/json; charset=utf-8&quot;,
  11.     dataType: &quot;json&quot;,
  12.     success: function(msg) {
  13.       // msg.d will contain the total number of items.
  14.       // Divide and round up to find total number of pages.
  15.       lastPage = Math.ceil(msg.d / pageSize);
  16.  
  17.       // Wireup appropriate paging functionality.
  18.       UpdatePaging();
  19.     }
  20.   });
  21. }

Now that we’re doing things more correctly, it makes more sense to initialize the lastPage variable at one. Until we’ve called our new GetFeedBurnerItemCount method and know the size of the data, we have no reason to assume there is more than one page of data.

To put this in action, we can add it to the $(document).ready handler:

  1. $(document).ready(function() {
  2.   // On page load, display the first page of results.
  3.   DisplayRSSTable(1);
  4.  
  5.   // Simultaneously, begin loading the total item count.
  6.   GetRSSItemCount();
  7. });

The end result is that both paging controls will originally be in a disabled state, until GetRSSItemCount is able to verify that at least (pageSize + 1) items exist in the data source.

If you’re implementing paging against a relatively static data source, like my RSS feed, there’s no need to add this functionality. However, I’m providing the added flexibility in hopes that it will make this more easily adaptable to your particular situation.

Conclusion

This has been a long post, but hopefully each individual part was digestible. As with most things, deconstructing the problem into manageable pieces makes the overall task easy. A few more thoughts include:

Error Handling. If you use this code, please do add error and timeout handling to your service calls. For the sake of brevity and clarity, I don’t address error handling in posts not specifically about that topic. However, you should never do that in production.

Caching. Depending on your data source, you may want to make use of the Cache class in your page method. For instance, this RSS example would benefit massively from Caching. However, you may just as likely be paging into a huge data source, in which case it wouldn’t make any sense to Cache.

Do keep it in mind though. With caching, paging through this example is almost instantaneous after the initial load.

That’s it. I hope you found this helpful.

Download the source

client-paging.zip (38kb)

###

Originally posted at Encosia.com. If you're reading this on another site, come on over and take a look at the real thing.

Easily build powerful client-side AJAX paging, using jQuery

How to easily enhance your existing tables with simple CSS

Posted on August 13, 2008 by Dave Ward.
Categories: ASP.NET, Contributors.

In two of my recent posts, I demonstrated using ASP.NET AJAX in conjunction with jQuery or ASP.NET AJAX 4.0’s templating features to generate client side tables from JSON data. However, what I neglected to show you was how to make them look even remotely presentable.

In this post, we’ll continue where those posts left off. I’m going to show you how you can improve the table’s presentation in three steps:

  • Setting basic styles for the page and table.
  • Improving the table header by using strong contrast.
  • Adding subtle gridlines to the table body.

A humble beginning and a basic foundation

At the end of both my previous posts on client-side templating, you were left with an HTML table that looked like this:

Though the table’s semantic structure is excellent, it is visually abysmal. Luckily, the table’s markup is very well suited to extensive CSS styling, so improving its presentation will be relatively painless.

Getting started, I’m going to apply styles to the container div and root table element, giving them a fixed width and centering them. I’ll also specify a better font than Times New Roman:

  1. /* No one likes Times New Roman, including me. */
  2. body { font-family: &quot;Trebuchet MS&quot;, Serif; }
  3.  
  4. /* Gives the container a fixed
  5.     width and centers it on the page. */
  6. #Container { margin: 0 auto; width: 900px; }
  7.  
  8. /* Expand the table to fill the container, and
  9.     add a 2px black border around its outside edge. */
  10. table { width: 100%; border: 2px solid #000; }

Note that adding this border style to the table through CSS is not the same as setting the table element’s border attribute to 2. This CSS will add a border only to the outside of the table, whereas the table’s border attribute would add borders around and between every cell.

Use contrast to improve the <thead>

By default, most browsers will render a <thead>’s <th> elements in a bold and slightly larger font style. That’s a good start, but we can do better:

  1. table thead tr th {
  2.   /* Set the heading's background image to something nice.
  3.       Use a similar background color, to maintain continuity. */
  4.   background-color: #000;
  5.   background-image: url(images/black-gradient.png);
  6.  
  7.   /* Set the foreground color to something readable on black. */
  8.   color: #FFF;
  9.  
  10.   /* Give the heading a larger, bold style */
  11.   font-size: 18px;
  12.   font-weight: bold;
  13.  
  14.   /* Finally, make it the same height as the background image. */
  15.   height: 35px;
  16. }

This styling makes the <thead> area of the table look like this:

The dark background and reversed text both help to strongly define the heading row and draw your eye to the table’s intended starting point. Unless you have a captive audience, minimizing the time your users spend visually scanning the page is vitally important.

Since the background image covers the entire height of the <thead>, setting a background color may seem redundant, but is a good precaution. In case the image loads slowly, times out, or even 404s, your table is still usable.

Subtle improvements to the <tbody>

The net benefit of zebra striping and gridlines inside table bodies has been a point of contention lately. I’m not entirely sold on zebra striping myself, but believe that subtle gridlines between table cells are a great usability gain.

  1. table tbody td {
  2.   /* Pure black is harsh on a white background. Soften slightly. */
  3.   color: #333;
  4.  
  5.   /* Give the &amp;lt;td&amp;gt; cell content some breathing room. */
  6.   padding: 4px;
  7.  
  8.   /* Create the illusion of gridlines between the cells. */
  9.   border-top: 1px solid #CCC;
  10.   border-left: 1px solid #CCC;
  11. }

The top and left cell borders fit together to give the illusion of continuous gridlines, similar to what the GridView produces: