Blog Archives

Using jQuery to enhance ASP.NET AJAX progress indication

Animated example of using jQuery's BlockUI with an UpdatePanelIn the spirit of Microsoft’s official embrace of jQuery last week, I’d like to show you an example of how easily you can add jQuery functionality to existing ASP.NET AJAX applications. jQuery allows you to achieve exceptional results with minimal difficulty, and integrating it with ASP.NET is not an exception.

My previous posts about jQuery have been somewhat lower level, incorporating JSON, web services, and the like. While these are important tools to have at your disposal, the realities of software development sometimes leave us scrambling for something quick and dirty.

In those situations, you may find yourself using the UpdatePanel.

To that end, I’m going to show you how easily you can use jQuery’s BlockUI plugin to display a modal progress indicator during an UpdatePanel’s partial postback.

Setting up a sample

new-customer-formTo avoid performance consequences associated with the UpdatePanel, judicious use is key. However, if you find that you need access to the values of several controls on the page, an UpdatePanel may practically be the best way to get the job done.

Take this new customer form (left) for example:

<div id="EntryForm">
  <label for="Name">Name:</label>
  <asp:TextBox runat="server" ID="Name" />
 
  <label for="Address">Address:</label>
  <asp:TextBox runat="server" ID="Address" />
 
  <label for="City">City:</label>
  <asp:TextBox runat="server" ID="City" />
 
  <label for="State">State:</label>
  <asp:TextBox runat="server" ID="State" />
 
  <label for="Zip">Zip:</label>
  <asp:TextBox runat="server" ID="Zip" />
 
  <asp:Button runat="server" ID="Save" OnClick="Save_Click"
    Text="Add Customer" />
</div>

Assuming the form isn’t something expected to handle heavy usage, this is the type of thing that an UpdatePanel excels at. If the feature were to become popular and suffer from performance trouble, the UpdatePanel could always be refactored out in favor of a web service.

Styling the form

This CSS provides layout and basic styling:

#EntryForm {
  border: 1px solid #999;
  padding: 10px;
  width: 300px;
}
 
  #EntryForm label {
    clear: both;
    float: left;
    margin-bottom: 5px;
    padding-right: 10px;
    text-align: right;
    width: 75px;
  }
 
  #EntryForm input[type=text] {
    float: left;
    margin-bottom: 10px;
    width: 150px;
  }
 
  #EntryForm input[type=button] {
    clear: both;
    margin-left: 85px;
  }

If you prefer using tables to lay out forms, that’s okay too. The jQuery technique described below will work equally well with either layout method.

Using the UpdatePanel to add asynchronicity

To asynchronously handle the server-side click event of the save button, add an UpdatePanel which specifies the save Button as an AsyncPostBackTrigger:

<asp:UpdatePanel runat="server" ID="up1">
  <Triggers>
    <asp:AsyncPostBackTrigger ControlID="Save" />
  </Triggers>
  <ContentTemplate>
    <asp:Panel runat="server" ID="ConfirmSave" Visible="false">
      <h3><asp:Literal runat="server" ID="CustomerName" /> added</h3>
    </asp:Panel>
  </ContentTemplate>
</asp:UpdatePanel>

By positioning the UpdatePanel so that the partial postback is limited to refreshing only what is necessary, you can mitigate as much of its overhead as possible.

The Literal control allows us to provide contextual information to the user after the save completes. Implementing the server-side functionality for our fictitious form might look something like this:

protected void Save_Click(object sender, EventArgs e)
{
  // Save the customer to your data store here.
  System.Threading.Thread.Sleep(2000);
 
  // Display the confirmation.
  ConfirmSave.Visible = true;
 
  // Customize the confirmation.
  CustomerName.Text = Name.Text;
}

After the customer is saved, the confirmation message will be updated and the Panel control that contains it will be made visible.

You may be tempted to skip the Panel control and directly toggle the UpdatePanel’s visibility property instead. Unfortunately, that won’t work. An UpdatePanel is able to refresh its contents, but not itself.

This visibility edge case is one that comes up often, and deserves some attention in ASP.NET AJAX 4.0. It shouldn’t be difficult for an UpdatePanel.Visible change to instruct the PageRequestManager to remove that UpdatePanel from the DOM.

Displaying the modal progress indicator with BlockUI

BlockUI’s functionality comes in two primary variations: blocking the entire page and blocking a particular element.

In this example, we’ll block the entry form’s container div, rather than the entire page. To invoke BlockUI on element(s), create a jQuery selector for the element(s) that you want to block and then call the .block() method on the resulting jQuery object.

To put this into action on our customer entry form, we need to add a client-side onclick handler to the save Button. To illustrate how effortlessly jQuery and ASP.NET AJAX integrate with each other, we can wire this event handler up in the Application.Init event:

Sys.Application.add_init(function() {
  // Allows the div.blockMsg style in CSS to
  //  override BlockUI's defaults.
  $.blockUI.defaults.css = {};
 
  // Add the BlockUI call as an onclick handler
  //  of the Save button.
  $addHandler($get('Save'), 'click', function() {
    $('#EntryForm').block({ message: null });
  });
});

If the jQuery-esque inline callback functions bother you, don’t worry. This style is interchangeable with what you’re used to seeing in most ASP.NET AJAX examples.

Using CSS to style the progress indicator

Screenshot of the styled BlockUI element, with progress indicatorFinally, let’s add a bit of custom styling to the BlockUI modal element. BlockUI’s defaults look pretty good, but we’ll need to change things around in order to get the animated progress indicator to display properly.

div.blockMsg {
  background-color: #fff;
  background-image: url(images/progress-indicator.gif);
  background-position: center center;
  background-repeat: no-repeat;
  border: 1px solid #ddd;
  height: 50px;
  width: 270px;
}

Gotcha: the event stops here

Because BlockUI attempts to suppress form submissions as part of its blocking functionality, the save Button’s partial postback will also be blocked. To remedy this, we must set the Button’s UseSubmitBehavior property to false:

<asp:Button runat="server" ID="Save" OnClick="Save_Click"
  Text="Add Customer" UseSubmitBehavior="false" />

This will cause ASP.NET to render the Button with an onclick handler that executes __doPostBack instead of relying on HTML’s underlying form submission event.

Hiding the progress indicator afterward

BlockUI won’t automatically unblock our form after the partial postback completes. So, we’ll need some code to do that at the end of the UpdatePanel’s refresh.

There are several ways to accomplish this. In this limited example, adding the following code to our existing Application.Init handler is the simplest way:

// Get a reference to the PageRequestManager.
var prm = Sys.WebForms.PageRequestManager.getInstance();
 
// Unblock the form when a partial postback ends.
prm.add_endRequest(function() {
  $('#EntryForm').unblock();
});

We could use ScriptManager.RegisterStartupScript to emit the same unblock code from Save_OnClick, but I’d caution against doing that.

Needlessly mixing client and server side code entangles your presentation and logic together, leaving you with maintenance headaches. If you can avoid doing that, you’ll appreciate it later on.

Alternate event handler coding styles

I’ve been using anonymous functions to declare event handler callbacks inline, instead of the traditional route of using standalone functions. This is a choice of coding style, and is not functionally significant.

For example, this is equivalent to the previous click handler code:

Sys.Application.add_init(AppInit);
 
function AppInit() {
  $.blockUI.defaults.css = {};
 
  $addHandler($get('Save'), 'click', Save_Click);
}
 
function Save_Click() {
  $('#EntryForm').block({ message: null });
}

You could also use jQuery’s events instead of ASP.NET AJAX’s:

$(document).ready(function() {
  $.blockUI.defaults.css = {};
 
  $('#Save').click(function() {
    $('#EntryForm').block({ message: null });
  });
});

Many jQuery examples will use the latter method, so it’s important to understand that they are all roughly interchangeable.

Ultimately, the choice is completely up to you. What’s your preference?

Conclusion

I hope that this example has served to show just how easily jQuery allows you to add slick presentational effects to your ASP.NET AJAX applications.

Considering the amount of overlap between them, both frameworks do a great job of peacefully coexisting with each other. There’s no need to worry about combining the two right away, and then gradually beginning to enhance your existing ASP.NET AJAX functionality with jQuery.

If you enjoyed this example, stay tuned. Due to length, this post only covers about half of what I originally wanted to demonstrate with BlockUI and the UpdatePanel. A followup post in the near future will explore how to extend the confirmation Panel with similar usability improvements.

Try it for yourself

jQuery-modal.zip

###

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

Using jQuery to enhance ASP.NET AJAX progress indication

Username Availability Validator v1.0 released

I am happy to announce that the first release of Username Availability Validator is available on CodePlex today: www.codeplex.com/UsernameAvailability.

Username Availability Validator is an ASP.NET server control which provides indication of username availability, for use in new user registration functionality.

Features include:

  • Choice of automatic validation of the username’s availability as the user types it or validation when the user changes form fields.
  • Derived from BaseValidator, so Page.IsValid and the ValidationSummary control function correctly.
  • Flexibility of validating against an ASP.NET MembershipProvider or a custom authentication store, through a user supplied web service or page method.

And the winner is…

With development on the first release of the control concluded, it’s time to award the first free copy of Adam Calderon and Joel Rumerman’s Advanced ASP.NET AJAX Server Controls.

Several developers made great contributions to the project, but two stood out from the rest: jnagy and vbtwo31984.

After conferring with Adam and Joel, the final verdict is that jnagy edged out the competition. Congratulations to our first winner, jnagy!

More books to win

Now that development is completed, it’s time to put together some respectable documentation. By contributing documentation to the project, you may join jnagy in receiving a free copy of the book.

I think this example that Moses put together recently is a great example of useful documentation. However, we need to also cover basics about the control’s various properties and their usage.

What do you think? If you were to stumble onto this control, what documentation would you like to have in order to make it easiest to implement quickly?

Email contributions to me, and I’ll add them to the project’s wiki. If you’d prefer editing the wiki directly, contact me with your CodePlex username and I’ll give you access to do that.

###

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

Username Availability Validator v1.0 released

v0.1 of my Visit/PageView Analysis Services Cube

So I created a cube using my the visit/pageview that I recently started collecting. The cube is VERY simple – only 4 dimensions {App, Page, Source, Time} and just a single measure – {Hit}. I created the cube using the 2005 versions of Visual Studio and Microsoft’s Analysis Services. The IDE’s wizards pretty much walk you through the process, which is great because creating an Analysis Services project from scratch is more than a little intimidating. Especially if your a web developer like me and you don’t know a whole lot about querying, let alone designing a cube.

Anyway, like I said, my cube is very simple. The App dimension only contains 2 members: ‘mattberseth’ for this site, and ‘mattberseth2′ for my live demo site. The Page dimension contains all of the unique URLs for both sites as its members, the Source dimension is essentially a bit field for determining if the traffic was the result of a direct hit or from a referring site and finally the Time dimension represents the calendar and is used for counting hits by a time interval (i.e. Days, Weeks, Months, etc…).

In my simple cube, all 4 dimensions and the Hit measure are currently coming from a single table. I have simulated the standard star schema by generating the keys for the different dimensions using the following 4 SQL queries. So the first query in the SQL snippet populates my measure group and the other three are responsible for populating the App, Source and Page dimensions.

-- builds the fact_pageview measure group ***
select
    -- if the referring url is empty than we know the source comes from a direct hit
    (case when ref_url = '' then 1 else 0 end) as source_id,
    -- the app_id - either mattberseth or mattberseth2
    app_id,
    -- turn the page url to an int
    checksum(url) as page_id,
    -- extract the date portion of the datetime
    convert(datetime, floor(convert(float, date))) as date,
    -- each row is a sinlge hit
    1 as hit
from
    -- my visit table
    visit_load with(nolock)

-- builds the app dimension ***
select
    'mattberseth' as app_id,
    'My Blog' as app_name
union
select
    'mattberseth2',
    'My Live Demo Site'

-- builds the source dimension ***
select
    0 as source_id,
    'Referrer' as source_name
union
select
    1,
    'Direct'

-- builds the page dimension ***
select
    checksum(url) as page_id,
    url as url
from
    visit_load with(nolock)

Next, I created views for these 4 queries, let my Analysis Services project know about them and used them as the data source for my cube. Conceptually, this diagram shows how these 4 queries are related.

image

Browsing and Querying the Cube

And amazingly, only after a few minutes of nexting through wizards and drag and drop design work, I deployed and processed the cube to my local Analysis Server instance. And now I can start taking a look at the data.

Browsing the Cube

Once the cube is deployed and processed, you can start browsing it. Below are a couple of screen shots that show the structure of my cube on the left, and hit counts for my two sites (mattberseth.com and mattberseth2.com) segmented by the traffic source (either direct traffic or referring site). The screen shot below that shows these counts as a percentage of the grand total. Looks like direct traffic to my demo site only makes up 2% of my total traffic ;(

image

image

Querying the Cube

And if you can stomach writing a little MDX, you can write custom queries to extract even more useful information. Below is a sample MDX query and result set that shows the average traffic per day for both mattberseth.com and mattberseth2.com segmented by week. The numbers are a little deceiving because only Week 39 consists of a full 7 days, but I think you can get the picture.

with
    -- define the Weekend and Weekday sets
    set [Weekday] as
    {
        [Time].[Day Of Week].[Day 2],
        [Time].[Day Of Week].[Day 3],
        [Time].[Day Of Week].[Day 4],
        [Time].[Day Of Week].[Day 5],
        [Time].[Day Of Week].[Day 6]
    }
    set [Weekend] as
    {
        [Time].[Day Of Week].[Day 1],
        [Time].[Day Of Week].[Day 7]
    }
    -- create a few calculated meausres based that make use of these sets
    member [Measures].[Weekday Average] as avg([Weekday], [Measures].[Hit]), format_string = '#'
    member [Measures].[Weekend Average] as avg([Weekend], [Measures].[Hit]), format_string = '#'
    member [Measures].[Weekly Average] as avg({[Weekday], [Weekend]}, [Measures].[Hit]), format_string = '#'
select
{
    [Measures].[Weekday Average],
    [Measures].[Weekend Average],
    [Measures].[Weekly Average]
} on 0,
non empty
{
    [App].children * [Time].[Week Of Year].children
} on 1
from
    [PageView]

image

What’s Next?

Well, I am pretty excited. I only have a handful of development hours invested in my visit cube (it honestly took longer to write this post than it did to create the cube) and I can already tell I have made the right decision by maintaining my own pageview/visit database. Of course there is still a lot to do …

  • My pageview JavaScript tracking code needs some work. I have been tweaking it over the past 2 weeks to play around with different techniques to keep my tracking request from getting cached. I have finally come within a few percent of what Google Analytics is reporting so I am happy. I think I will clean the handler up and write a quick post describing what I did.
  • My Source dimension on has 2 members – Direct and Referrer. I would like to break down the Referrer further to include Search Engines, Community (DNK, Reddit, Digg, DZone, etc…), and forums (forums.asp.net, stackoverflow, etc…).

  • I need to extract keywords from the Search Engine sources and get them into the cube
  • Look up geography information based on IP
  • IP + User Agent Sessionization. I would like to track time on site, navigation paths, etc…
  • Incorporate additional dimensional data from my Moveable Type database

At some point I plan on sharing the solution: JavaScript tracking code, HttpHandler, OLTP and OLAP databases as well as the Analysis Services Project …

That’s it. Enjoy!

Avoid this tricky conflict between ASP.NET AJAX and jQuery

ASP.NET and jQuery's logos togetherYou have probably already read the great news that Microsoft is going to begin shipping jQuery with Visual Studio and ASP.NET MVC. If not, make sure you take a minute to read the official announcements from both ScottGu and John Resig. This represents a surprising, yet tremendously welcomed change of course for Microsoft.

If you haven’t yet used jQuery with ASP.NET, you’re in for a pleasant surprise. It removes almost all of the pain from client-side development. Coming from an ASP.NET centric perspective, you may find several of my previous jQuery articles useful.

Additionally, I highly recommend the articles that Matt Berseth and Rick Strahl have written on the topic of using jQuery with ASP.NET. I am constantly amazed at the quantity and quality of content that they both generate.

Introductions aside, I’d like to take this opportunity to discuss an incompatibility between ASP.NET AJAX and at least one jQuery plugin, which needs to be fixed.

A simple combination leads to an odd problem

Last month, I came across someone having an odd problem with ASP.NET AJAX and jQuery, on the ASP.NET forums. While using the jDrawer plugin to jQuery, he found that adding an ASP.NET AJAX ScriptManager completely broke jDrawer.

Having never encountered trouble mixing ASP.NET AJAX and jQuery plugins, I was reluctant to place the blame on ASP.NET AJAX until I tried it for myself. However, the problem was readily reproducible by downloading the jDrawer sample and adding a ScriptManager. It immediately went from a working demo to throwing this JavaScript error:

arguments[i] is undefined

Inspecting the specified section of the jDrawer code revealed this function:

PreloadImages: function() {
  for (var i in arguments.length)
    if (arguments[i].type === "array")
      for (var j in arguments[i])
        // Irrelevant image preloading code.
  else
    // Irrelevant image preloading code.
},

It looks simple enough, but we have a problem here…

Understanding the problem

In JavaScript, every function call is accompanied by a special arguments property which consists of an array of any parameters that the function was called with. This allows for functions with a variable number of parameters, like String.format.

jDrawer’s PreloadImages function is dependent on this arguments property. The property is necessary so that an arbitrary number of image URLs may be passed in for preloading. Unfortunately, the plugin author didn’t quite get the for loop right. He’s using the associative form (foreach), but arguments.length is a Number, not an array.

As I’ve previously covered, one of the nice things that ASP.NET AJAX brings to the table is an assortment of upgrades to JavaScript’s base types. Among them, the Number type is modified to include a format property, the root of this trouble:

Using Firebug to debug the jDrawer JavaScript error

Notice that even though the arguments property is an empty array, execution has entered the for loop. The reason for that? The jDrawer code is attempting to iterate over the Number, arguments.length, not the empty array itself.

Normally, this would cause the function to silently abort, since the base Number type has nothing to iterate over. However, ASP.NET AJAX has added a few features to the Number type, giving the faulty code the extended Number type’s properties to iterate over.

Because those certainly aren’t valid keys for the arguments array, execution always fails when line 424 tries to reference arguments[i] for the first time. Basically, it’s attempting to access arguments['format'], which throws the error.

The solution is easy!

The solution is to iterate over the arguments array using a traditional for loop:

PreloadImages: function() {
  for(var i = 0; i++; i < arguments.length)
    if (arguments[i].type === "array")
      for (var j in arguments[i])
        // Irrelevant image preloading code.
  else
    // Irrelevant image preloading code.
},

The troublesome format key is ignored, and the function operates as desired.

As a bonus, fixing the error also fixes the image preloading. Even though it wasn’t causing an error without a ScriptManager on the page, the preloading code in this plugin was never working before.

Conclusion

Don’t get me wrong — nothing this minor could possibly begin to dampen my excitement over jQuery getting this official nod from Microsoft.

However, with today’s news about official jQuery support and Microsoft’s efforts toward peaceful client-side coexistence in general, I think this issue is something that should be addressed if at all possible.

It’s not the ScriptManager’s fault that this happened, but it isn’t a problem unless the ScriptManager is present. Not many developers are going to track down obscure JavaScript errors caused by two third party pieces of code. They’ll see the cause of adding the ScriptManager, the effect of it breaking the page, and logically lay the blame on the ScriptManager (unfairly).

Hopefully, having the problem and solution identified here will mean that anyone who runs into it in the meantime will be able to fix it with a quick Google search.

As mentioned previously, I have several posts on using jQuery with ASP.NET AJAX that may be helpful if you’re just getting started with the combination.

If the topic interests you, be sure that you’re subscribed to Encosia updates via either my RSS feed or its email-based counterpart. More posts on ASP.NET and jQuery are on the way soon.

###

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

Avoid this tricky conflict between ASP.NET AJAX and jQuery

Dynamic Data Demos Now Available

Last week DiscountASP upgraded to .Net 3.5 SP1 – so I took sometime this weekend to publish demo's for my 5 Dynamic Data posts.  Here is a quick summary of what is out there … 

 

ASP.NET Dynamic Data – Simple 5 Table Northwind Example

Very simple DD site based on the 5 core Northwind tables.  Read more …

Download | Live Demo

image

 

Dynamic Data and Custom Metadata Providers

DD enabled controls take rendering hints from the metadata you apply to your data model.  This post shows how you can create a custom metadata provider that will provide sensible default values for the DisplayName and DisplayFormat attributes.  Read more …

Download | Live Demo

image

 

A Dynamic Menu For Your Dynamic Data

This post continues the custom metadata theme and shows how you can use the existing Category attribute to create a 2 level navigation bar.  Read more …

Download | Live Demo

image

 

Dynamic Data – Customizing the Delete Confirmation Dialog

Example that shows how you can use the jQuery thickbox to show a delete confirmation dialog.  Read more …

Download | Live Demo

image

 

Dynamic Data – Experimenting with YUI’s DataTable and DataSource Controls

Exploring how the YUI DataTable and DataSource widgets could be used from a DD site.  Read more …

Download | Live Demo

image

 

That's it.  Enjoy!

Dynamic Data – Experimenting with YUI’s DataTable and DataSource Controls

I spent a few hours putting together a Dynamic Data web site using the YUI DataTable and DataSource components.  I mostly just did this out of curiosity to see how easy or difficult it would be to use the DD API with other components besides the GridView and DetailsView controls.  So I created a DD web site that provides read-only access to Northwind's Customer, Employee and Supplier tables, but I am not using ASP.NET's GridView, DataSource and UpdatePanel controls to render the grid.  Instead I have replaced these components with YUI's client side DataTable and DataSource and serve the data using a web service (paging included!).  Below are a few of the highlights, and don't forget to download the sample and try it out for yourself.  Its experimental, but if you are new to DD or YUI you might find it interesting. 

[Update: 9/21/2008]: Added live demo link 

Download | Live Demo

image  

 

The List Page Template

The markup for my List page template is pretty simple and fairly similar to the DD sites I have blogged about previously.  I use the table's DisplayName and Description attributes to render the title bar.  But, for this sample I am also including three extra DIV's – #paging-top, #grid and #paging-bottom.  These three DIV's will be used as the containers for the YUI pager and grid widgets. 

image

After the markup is added, I included a bit of JavaScript for configuring the YUI DataTable and DataSource components.  The source for the script is below.  There is quite a bit going on so right after the source is a line-by-line guide of what it is doing …

   1: //  render the YUI script ...
   2: YAHOO.util.Event.addListener(window, "load", function() {
   3:     
   4:     //  create the datasource and point it at the webservice's FetchAll method
   5:     var myDataSource = new YAHOO.util.DataSource('/dd_yui/NorthwindService.asmx/FetchAll', { connMethodPost: true });
   6:     
   7:     //  set the content-type to JSON
   8:     myDataSource.connMgr = YAHOO.util.Connect;
   9:     myDataSource.connMgr.initHeader('Content-Type', 'application/json; charset=utf-8', true);
  10:     myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;            
  11:     
  12:     //  setup the response schema
  13:     myDataSource.responseSchema = 
  14:     { 
  15:         resultsList : 'd.records',
  16:         fields: <%= this.FetchDataFields() %>,
  17:         metaFields : { totalRecords: 'd.totalRecords' }                 
  18:     };
  19:    
  20:     //  configure the data pager
  21:     var myPaginator = new YAHOO.widget.Paginator({
  22:         //  2 data pagers - both top and bottom
  23:         containers : ['paging-top', 'paging-bottom'],
  24:         pageLinks : 5,
  25:         rowsPerPage : 15,
  26:         template : "{PreviousPageLink} {PageLinks} {NextPageLink}"
  27:     });                       
  28:     
  29:     //  setup the table settings
  30:     var myTableConfig = {
  31:         //  JSON object that maps to the parameters of my WebMethod
  32:         initialRequest : '{startIndex:0, pageSize:15, tableName:"<%= this.table.Name %>"}',
  33:         // A custom function to translate the js paging request into a JSON
  34:         generateRequest : function(state, dt){
  35:             return '{startIndex:' + state.pagination.recordOffset + ', pageSize:' + state.pagination.rowsPerPage + ', tableName:"<%= this.table.Name %>"}'
  36:         },
  37:         paginator : myPaginator,
  38:         paginationEventHandler : YAHOO.widget.DataTable.handleDataSourcePagination
  39:     };            
  40:     
  41:     //  create the datatable
  42:     this.myDataTable = new YAHOO.widget.DataTable("grid", <%= this.FetchColumnDefinitions() %>, myDataSource, myTableConfig);
  43: });
  • Line 5 – 10:  Create the YUI DataSource. 
    • The YUI DataSource can grab data from just about anywhere – an XML DOM object, JavaScript array, an HTML Table, or from a remote server.  For this example I created a FetchAll web method that will serve the Northwind data so I have configured the DataSource to use this web method.
  • Lines 13 – 18: Describe what the response is going to look like.
    • You need to tell the DataSource what the elements returned from your data look like.  In lines 15 – 17 I am letting the DataSource know what it can expect to be returned from my web service.  If you notice the fields array is dynamically created by invoking the FetchDataFields method on my List.aspx template page.  FetchDataFields queries the MetaTable for the current request and includes the names of all of the columns that need to be scaffolded.  Here is the source for this method.
    • Notice that I am retrieving the reference to the MetaTable from the DynamicDataRouteHandler – keeping my template unaware of the entity type the page is being requested for (line 4)
   1: public string FetchDataFields()
   2: {
   3:     //  get a reference to the MetaTable for the current route/request
   4:     MetaTable table = DynamicDataRouteHandler.GetRequestMetaTable(HttpContext.Current);
   5:  
   6:     var columns =
   7:         from c in table.Columns
   8:         where c.Scaffold
   9:         select c.Name;
  10:  
  11:     return new JavaScriptSerializer().Serialize(columns.ToArray());
  12: }
  • Lines 21 – 27: Initialize the data pager controls. 
    • I wanted to use 2 pagers – one above the grid and another below it so I added DIV's to the List template for both of these items.  When I create the Paginator widget I supply YUI with the ID's of the DIV's that should contain the paging controls.
  • Lines 30 – 39: Setup the config options for the table.
  • Line 42: Create the DataTable.
    • I need to tell the DataTable about some basic metadata regarding the grids columns.  For this example I have included only the most basic information – the mapping between my column headers and the fields from the object returned from my web method that fills the grid …
   1: public string FetchColumnDefinitions()
   2: {
   3:     //  get a reference to the MetaTable for the current route/request
   4:     MetaTable table = DynamicDataRouteHandler.GetRequestMetaTable(HttpContext.Current);
   5:  
   6:     //  grab the names of the columns we are scaffolding.
   7:     //  YUI uses the key/label to map the fields returned
   8:     //  by our web method to the columns in the DataTable
   9:     var columns =
  10:         from c in table.Columns
  11:         where c.Scaffold
  12:         select new
  13:         {
  14:             key = c.Name,
  15:             label = c.DisplayName,
  16:             resizable = true
  17:         };
  18:  
  19:     return new JavaScriptSerializer().Serialize(columns.ToArray());
  20: }
    • Finally, I use all of the settings and objects created previously to initialize a new DataTable instance – pointing it to the #grid DIV that I have already added to the page

 

The FetchAll Web Method

Finally, here is the source listing for my FetchAll web method.  I have configured the YUI DataSource to pass through the name of the table to retrieve the data for.  This is what is allowing me to use the same web method for all of the tables.  So this is the first parameter, the second and third parameters are the paging arguments.  Other than that the method is pretty simple …

  • Line 6 – 8: Use the tableName argument to get the MetaTable its corresponding IQuerable objects
  • Line 11: We need to total row count for our paging math – so execute a count(*)
  • Line 14: We only need to retrieve the columns we are scaffolding, so get the list of columns we want to include in the select
  • Line 20 – 26: Create the object that we will return to the YUI DataSource
    • Include the total row count
    • Use Skip and Take to handling the paging parameters
    • Use Select to make sure only the columns we are using are being returned
   1: [WebMethod]
   2: [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
   3: public object FetchAll(string tableName, int startIndex, int pageSize) 
   4: {
   5:     //  get the MetaTable for this table
   6:     MetaTable table = MetaModel.GetModel(typeof(NorthwindDataContext)).GetTable(tableName, typeof(NorthwindDataContext));
   7:     //  get the query
   8:     IQueryable query = table.GetQuery();
   9:  
  10:     //  total row count - used for the paging controls
  11:     int rowCount = query.Count();
  12:  
  13:     //  only pull back the columns we are scaffolding
  14:     var columns = 
  15:         from c in table.Columns
  16:         where c.Scaffold
  17:         select c.Name;
  18:  
  19:     //  return the collection
  20:     return new
  21:     {
  22:         //  include the total row count
  23:         totalRecords = rowCount,
  24:         //  get the current page
  25:         records = query.Skip(startIndex).Take(pageSize).Select(string.Format("new({0})", string.Join(",", columns.ToArray())))
  26:     };
  27: }

 

Conclusion

Well, like I said it was just an experiment.  Probably not ready for production and I haven't quite worked out how the edit/delete scenarios are going to work, but I thought it was interesting enough to write up a quick post about.  And DiscountASP is upgrading to .NET 3.5 SP1 sometime this week, so I should have the demo’s up for my DD posts soon.

 

That's it.  Enjoy!

Dynamic Data – Customizing the Delete Confirmation Dialog

I spent some time customizing the delete confirmation dialog in the Dynamic Data site I have been blogging about recently. Specifically, I looked at …

  • replacing the browsers default confirm dialog with a jquery thickbox
  • displaying a confirmation message that includes contextual information regarding the row being deleted

Below is a screen shot showing how it turned out. You can read on for the details, or you can download the site and browse the code for yourself. Hopefully DiscountASP will get upgraded to .Net 3.5 SP1 soon so I can get back to providing demos as well …

Download

image

The Default Confirmation Dialog

If you use the Dynamic Data Web Site template to create your DD web site, the List.aspx page template will already include a default delete confirmation dialog that uses the browsers confirm dialog to prompt the user before allowing them to delete a record. Nothing too fancy here, the alert box just displays some static text (the same text is displayed for all rows). If the user clicks the OK button the record is deleted. Here is what that looks like …

image

And the markup for generating this dialog is also very straight forward …

Customizing the Confirmation Message

With just a small code change, we can make use of a few DD features to add some contextual information about the row being deleted to the confirmation message. You can ask DD to retrieve a display string for any of the rows that are bound to your grid. So, with a pretty simple change we can enhance our dialog to include the display string for the row being deleted. Here is a quick example of what this looks like …

image

And here is the markup for generating this …

Notice the change to OnClientClick. Instead of static text, I am asking DD’s MetaTable (the instance the grid is bound to) to retrieve the display string that corresponds to the rows data item. Simple.

So how does DD know what the display string is? Well, you have to tell it. And you have a couple of options when doing this …

  • Override the ToString property on your entity. Below is a partial class for the Northwind Employee entity. DD will detect that I have overridden the ToString method and will use it whenever the display name is requested for an Employee.
[MetadataType(typeof(EmployeeMetadata))]
public partial class Employee
{
    public override string ToString()
    {
        return string.Format("{0} {1}", this.FirstName, this.LastName);
    }
}

And this is how this shows up in the dialog …

image

  • Apply the DisplayColumn attribute to your entities metadata class. If you don’t need to do any computations or formatting and just want to return a column value you can apply the DisplayColumnAttribute to your entities meta-class and set the DisplayColumn value to the column value you want to use for display purposes. Below is a quick example where I am using the Customer’s ContactName column as the display value (line 5) …
[Category("People")]
[Description("You can use this page to find out what your customers are up to")]
[DisplayName("Customers")]
//  Use the ContactName column for the display name
[DisplayColumn("ContactName")]
public class CustomerMetadata
{
    [DisplayName("ID")]
    public object CustomerID { get; set; }
    [DisplayName("Company")]
    public object CompanyName { get; set; }
    [DisplayName("Name")]
    public object ContactName { get; set; }
    [DisplayName("Title")]
    public object ContactTitle { get; set; }

    //  Columns I want hidden
    [ScaffoldColumn(false)]
    public object Fax { get; set; }
    [ScaffoldColumn(false)]
    public object Phone { get; set; }
    [ScaffoldColumn(false)]
    public object PostalCode { get; set; }
    [ScaffoldColumn(false)]
    public object Country { get; set; }
    [ScaffoldColumn(false)]
    public object Orders { get; set; }
}

And this is how this shows up in the dialog …

image

The only caveat here is that if you have applied a custom format to the DisplayColumn using the DisplayFormat attribute, it doesn’t look like this custom formatting will be applied when the display string is retrieved. Probably not a big deal especially since you can apply formatting using the ToString method I mention above, but it is still interesting. Here is a quick example of what I mean …

I have added the DisplayColumn attribute to my Customer metadata class like so …

//  Use the ContactName column for the display name
[DisplayColumn("ContactName")]
public class CustomerMetadata {}

And applied the DisplayFormat attribute to the ContactName property like so …

[DisplayFormat(DataFormatString = "{0} testing")]
public object ContactName { get; set; }

Then when I request the DisplayString for the Customer rows, the testing suffix is not applied …

image

I didn’t expect that. But like I said, it really isn’t a huge deal because you can always use ToString to format the column value.

Plugging in jquery’s Thickbox

And finally, if you wish to use a different dialog box instead of the browsers default you can plug that into the List.aspx page template as well. I choose to use jquery’s thickbox, but if you prefer the AjaxControlToolkit’s ModalPopup you could easily implement that as well. Here is what I did to make use of the thickbox.

  • Add the markup for the grids ItemTemplate. There are 3 main controls in my template, a link that when clicked displays the thickbox, a hidden ASP button control that is used to force the postback that causes the row to be deleted and a hidden panel that displays the delete message for the row.

image

  • Next, I added a bit of code that runs as the grids rows are bound to the data items. I first extract the thickbox hyperlink, hidden delete button, the confirm button and the panel that contains the message. After I have all of these controls I populate the NavigateUrl property on the Hyperlink to include the correct options (including the ClientID of the rows confirmation message) and use the OnClientClick property of the Yes button to issue the delete by causing the hidden delete button to initiate a postback.
protected void RowDataBound(object sender, GridViewRowEventArgs args)
{
    if (!table.IsReadOnly && args.Row.RowType == DataControlRowType.DataRow)
    {
        //  track down the controls
        HyperLink deleteLink = (HyperLink)args.Row.FindControl("btnDelete");
        Button deleteCommand = (Button)args.Row.FindControl("deleteCommand");
        Button yes = (Button)args.Row.FindControl("yes");
        Panel deleteConfirm = (Panel)args.Row.FindControl("deleteConfirm");

        //  point the thickbox at the confirmation panel for this row
        const string thickBoxOptionsFormat = "#TB_inline?height=90&width=350&inlineId={0}&modal=true";
        deleteLink.NavigateUrl = string.Format(thickBoxOptionsFormat, deleteConfirm.ClientID);

        //  when the user clicks OK, locate the hidden Delete button
        //  and click it
        const string deleteFormat = "$('#{0}').click(); return false;";
        yes.OnClientClick = string.Format(deleteFormat, deleteCommand.ClientID);
    }
}

Conclusion

Again, I am pleased with the flexibility that Dynamic Data provides me. It’s these seemingly simple requirements of providing a custom delete message that I figured would be difficult to do with Dynamic Data.

That’s it. Enjoy!

A Dynamic Menu For Your Dynamic Data

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 …

[MetadataType(typeof(ProductByCategoryMetadata))]
public partial class Products_by_Category { }

[DisplayName("Products By Category")]
public class ProductByCategoryMetadata
{
    [ScaffoldColumn(false)]
    public object Discontinued { get; set; }
}

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 …

[MetadataType(typeof(ProductByCategoryMetadata))]
public partial class Products_by_Category { }

[Category("Products")]
[Description("You can use this page to view your products by category")]
[DisplayName("Products By Category")]
public class ProductByCategoryMetadata
{
    [ScaffoldColumn(false)]
    public object Discontinued { get; set; }
}

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) …

public static class MetaTableExtensions
{
    ///

    /// Gets the description for the MetaTable
    /// 

    public static string GetDescription(this MetaTable table)
    {
        return ((DescriptionAttribute)table.Attributes[typeof(DescriptionAttribute)]).Description;
    }

    ///

    /// Gets the category for the MetaTable
    /// 

    public static string GetCategory(this MetaTable table)
    {
        return ((CategoryAttribute)table.Attributes[typeof(CategoryAttribute)]).Category;
    }
}

… 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 …

protected void LdsMenu_Selecting(object sender, LinqDataSourceSelectEventArgs e)
{
    e.Result =
        from vt in MetaModel.Default.VisibleTables
        //  use the category to group the tables
        group vt by vt.GetCategory() into groups
        select new
        {
            CategoryName = groups.Key,
            Tables = groups
        };
}

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

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

//  Attach the Employee Metadata to the Employee
//  entity that the LINQ to SQL designer generates
[MetadataType(typeof(EmployeeMetadata))]
public partial class Employee
{
}

//  Attach some additional metadata
public class EmployeeMetadata
{
    //  Rename the EmployeeID column to Employee ID
    [DisplayName("Employee ID")]
    public object EmployeeID { get; set; }

    //  Format the Hire Date
    [DisplayFormat(DataFormatString = "{0:d}")]
    public object HireDate { get; set; }

    //  Hide the HomePhone column
    [ScaffoldColumn(false)]
    public object HomePhone { get; set; }
}

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.

//  Example 1:
//  just use the default metadata provider
model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
{
    ScaffoldAllTables = true
});

//  Example 2:
//  this is exactly the same as above
model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
{
    ScaffoldAllTables = true,
    MetadataProviderFactory = (type => new AssociatedMetadataTypeTypeDescriptionProvider(type))
});                

//  Example 3:
//  here I am using a custom provider that reads the metadata from
//  an xml file
model.RegisterContext(typeof(NorthwindDataContext), new ContextConfiguration()
{
    ScaffoldAllTables = true,
    MetadataProviderFactory = (type => new XmlMetadataDescriptionProvider(type, "metadata.xml"))
});  

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.
public override PropertyDescriptorCollection GetProperties()
{
    List propertyDescriptors = new List();

    foreach (PropertyDescriptor propDescriptor in base.GetProperties())
    {
        List newAttributes = new List();

        //  Display Name Rules ...
        //  If the property doesn't already have a DisplayNameAttribute defined
        //  go ahead and auto-generate one based on the property name
        if (!HasAttribute(propDescriptor))
        {
            //  generate the display name
            string friendlyDisplayName = ToHumanFromPascal(propDescriptor.Name);

            //  add it to the list
            newAttributes.Add(new DisplayNameAttribute(friendlyDisplayName));
        }

        //  Display Format Rules ...
        //  If the property doesn't already have a DisplayFormatAttribute defined
        //  go ahead and auto-generate one based on the property type
        if (!HasAttribute(propDescriptor))
        {
            //  get the default format for the property type
            string displayFormat = GetDisplayFormat(propDescriptor.PropertyType);

            //  add it to the list
            newAttributes.Add(new DisplayFormatAttribute() { DataFormatString = displayFormat });
        }

        propertyDescriptors.Add(new WrappedPropertyDescriptor(propDescriptor, newAttributes.ToArray()));
    }

    //  return the descriptor collection
    return new PropertyDescriptorCollection(propertyDescriptors.ToArray(), true);
}

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

//  Attach the OrderMetadata to the Order class
[MetadataType(typeof(OrderMetadata))]
public partial class Order {}

[TableName("My Orders")]
public class OrderMetadata
{
    //  Columns I want hidden
    [ScaffoldColumn(false)]
    public object RequiredDate { get; set; }
    [ScaffoldColumn(false)]
    public object ShipVia { get; set; }
    [ScaffoldColumn(false)]
    public object Freight { get; set; }
    [ScaffoldColumn(false)]
    public object ShipName { get; set; }
    [ScaffoldColumn(false)]
    public object ShipPostalCode { get; set; }
    [ScaffoldColumn(false)]
    public object ShipCountry { get; set; }
}

and this configuration …

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

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 …

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

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

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 …

public static void RegisterRoutes(RouteCollection routes)
{
  MetaModel model = new MetaModel();
  // IMPORTANT: DATA MODEL REGISTRATION
  // Uncomment this line to register LINQ to SQL classes or an ADO.NET Entity Data
  // model for ASP.NET Dynamic Data. Set ScaffoldAllTables = true only if you are sure
  // that you want all tables in the data model to support a scaffold (i.e. templates)
  // view. To control scaffolding for individual tables, create a partial class for
  // the table and apply the [Scaffold(true)] attribute to the partial class.
  // Note: Make sure that you change "YourDataContextType" to the name of the data context
  // class in your application.
  model.RegisterContext( typeof(NorthwindDataContext), new ContextConfiguration() { ScaffoldAllTables = true });

  // The following statement supports separate-page mode, where the List, Detail, Insert, and
  // Update tasks are performed by using separate pages. To enable this mode, uncomment the following
  // route definition, and comment out the route definitions in the combined-page mode section that follows.
  routes.Add(new DynamicDataRoute("{table}/{action}.aspx") {
    Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert"}), Model = model
  });

  // The following statements support combined-page mode, where the List, Detail, Insert, and
  // Update tasks are performed by using the same page. To enable this mode, uncomment the
  // following routes and comment out the route definition in the separate-page mode section above.
  //routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx") {
  // Action = PageAction.List,
  // ViewName = "ListDetails",
  // Model = model
  //});

  //routes.Add(new DynamicDataRoute("{table}/ListDetails.aspx") {
  // Action = PageAction.Details,
  // ViewName = "ListDetails",
  // Model = model
  //});

 }

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
// Attach the OrderMetadata to the Order class
[MetadataType(typeof(OrderMetadata))]
public partial class Order {}

[TableName("My Orders")]
public class OrderMetadata
{
  // Override the display name
  [DisplayName("Date Ordered")]

  // Format the Date
  [DisplayFormat(DataFormatString="{0:d}")]
  public object OrderDate { get; set; }

  // Override the display name
  [DisplayName("Date Shipped")]

  // Format the Date
  [DisplayFormat(DataFormatString = "{0:d}")]
  public object ShippedDate { get; set; }

  // Override the display name
  [DisplayName("Address")]
  public object ShipAddress { get; set; }

  // Override the display name
  [DisplayName("City")]
  public object ShipCity { get; set; }

  // Columns I want hidden
  [ScaffoldColumn(false)]
  public object RequiredDate { get; set; }

  [ScaffoldColumn(false)]
  public object ShipVia { get; set; }

  [ScaffoldColumn(false)]
  public object Freight { get; set; }

  [ScaffoldColumn(false)]
  public object ShipName { get; set; }

  [ScaffoldColumn(false)]
  public object ShipPostalCode { get; set; }

  [ScaffoldColumn(false)]
  public object ShipCountry { get; set; }
}

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!

WP Like Button Plugin by Free WordPress Templates