Tuesday, November 10, 2009

ASP.Net Dynamic Data (LINQ) Tips and Tricks

Dynamic data is a technology that enables RAD (rapid application development) for data-driven applications. What is a data-driven application, or rather, when does it make sense to use ASP.NET Dynamic Data?

Any type of CRUD (create/read/update/delete) application is a prime candidate for dynamic-driven applications. In fact, if you are building an internal site simply to administer or configure certain data stores, dynamic data is a very viable solution.

There is a major misconception that data driven entities relies on generated code and therefore suffers from the same issues many of the older "code generators" did. The reality is that dynamic data can live side-by-side with a traditional application and is extremely extensible. There are plenty of customization hooks that can empower you to rapidly generate a framework yet still maintain control over the user experience, validation, and business logic.

This post is for those of you either working with or considering using dynamic data. These are just a few tips based on my personal experience that may help with building your dynamic data solution. I am using LINQ to SQL for this example, but most of these tips are applicable to the entity framework flavor as well.

Adding Table MetaData

The first and key step to understand is how to extend your generated tables with meta data. There is no way to directly apply meta data to the LINQ generated classes, but with a little bit of magic we can use a custom meta data class.

Step One: create a partial class with the same name as your generated table. In the dbml code-behind, the table will be declared something like this:

[Table(Name="dbo.MyTable")]
public partial class MyTable : INotifyPropertyChanging, INotifyPropertyChanged 

In this case, I will create a MyTable.cs file. It is important this lives in the same namespace as your LINQ class (navigate to the top of the designer.cs class to see what the namespace is) and is declared as a partial class.

Your class will be empty, like this:

public partial class MyTable
{
    
}

Step Two: We will create a metadata class to help provide additional hints to the engine. I like to put this class into the same .cs file as the partial class to make it easier to understand and manage. Now our code will look like this:

public partial class MyTable
{
    
}

public class MyTableMetaData 
{

}

Now the "glue" to tie the LINQ class to the meta data class. You must use System.ComponentModel and System.ComponentModel.DataAnnotations, then decorate the extended LINQ class with the MetadataType attribute. The code looks like this:

[MetadataType(typeof(MyTableMetaData))]
public partial class MyTable
{
    
}

public class MyTableMetaData 
{

}

Now the metadata is "glued" to the LINQ class. So, let's do our first customization and change the table name so it doesn't show "MyTable" in the UI, but instead displays as "My Table":

[MetadataType(typeof(MyTableMetaData))]
[DisplayName("My Table")]
public partial class MyTable
{
    
}

public class MyTableMetaData 
{

}

Customizing the Field Template

If you navigate in your dynamic data solution to DynamicData -> FieldTemplates you'll find a collection of user controls that the dynamic data engine uses to render the fields. All of these are customizable. For example, the default textbox is a fixed width. You may want to size it to fit the width of the container. In the CSS, you can add a class:

.cssTextBox {
   width: 100%; 
}

Then crack open the Text_Edit.ascx file and add the attribute: CssClass="cssTextBox" to the textbox definition. You can, of course, style the control even further as needed.

Foreign Key References

Dynamic data entities tries to make a "best guess" about which column of a table makes sense to display for a foreign key. For example, let's assume we have a "Student" class that associated with "Classroom." The dynamic data engine may "guess" that the student's phone number is the most appropriate field to show. Obviously, you would prefer to list their full name. To do this, simply decorate the table class (not the metadata class) with the DisplayColumn annotation, like this:

[DisplayColumn("FullName")]
public partial class Student 
{

}

Custom Controls

Creating custom controls is easy. You may have a particular field that is an integer value for a specific range. It makes more sense to use a slider control instead of the auto-generated textbox. Here are the steps to create your custom control:

Display, Edit, or Both?

Create a user control under DynamicData -> FieldTemplates. The format for these controls is controlname and controlname_edit. You do not have to supply both ... you might only want to override the display mode, or the edit mode. In this case, we're doing the edit mode, so you would create a user control called Slider_Edit.ascx.

Inherit from System.Web.DynamicData.FieldTemplateUserControl

This is required for your custom field control.

Override DataControl

This should return the main control holding the field state. For example, if your slider has the identifier MySlider, your override will look like:

public override Control DataControl 
{
   get 
   {
      return MySlider;
   }
}

You can get the value of the current field using FieldValueString.

For Edit Controls, Override ExtractValues

ExtractValues is the interface between your custom control and the data store. The ExtractValues method is called with a IOrderedDictionary that contains all of the columns in the data store. Your job is to to take a text value from your control and pass it back into the dictionary using the ConvertEditedValue helper method. In our slider example, if the value is exposed by SelectedValue, you would wire in the value like this:

protected override void ExtractValues(IOrderedDictionary dictionary) 
{
   dictionary[Column.Name] = ConvertEditedValue(MySlider.SelectedValue); 
}

Making a Field Readonly (Custom Control)

Now that we know how to customize our controls, here's how to make a field readonly.

  1. Navigate to DynamicData -> FieldTemplates
  2. Right click on Text.ascx and select "copy"
  3. Right click on the FieldTemplates folder and select "paste"
  4. Rename the pasted control to "ReadonlyText_Edit" and make sure the markup matches the code behind (you'll need to change the class name from TextField to ReadonlyTextField_Edit.
  5. Add the UIHint to the field you wish to make read only

How do we provide the hint? Navigate to the meta data object you created for the table, and decorate the attribute. In "MyTable" if I want to make "MyField" read only, I would do this:

[MetadataType(typeof(MyTableMetaData))]
public partial class MyTable 
{
}

public class MyTableMetaData 
{
   [UIHint("ReadonlyText")]
   public object MyField { get; set; }
}

Notice that I name the property I'm overriding, but declare it as a simple object. I'm not typing it here. The purpose of the meta data is provide annotiations as hints to the dynamic data engine. My hint tells it to use the ReadonlyText control. When displaying the field, the engine searches for ReadonlyText.ascx and cannot find it, so it defaults to the original target, Text.ascx. In edit mode, it finds ReadonlyText_Edit.ascx and therefore instantiates that control, which simply displays the field and does not allow editing.

Applying Validation Attributes

Applying validation attributes is simple. Let's assume we have a field that is required, and cannot exceed 50 characters in length. Applying these validations is as simple as:

public class MyTableMetaData 
{
   [Required]
   [StringLength(50,ErrorMessage="My field cannot exceed 50 characters in length.")]
   public object MyField { get; set; }
}

There is event built-in support for a regular expression validator!

Extending the User Experience

To extend the user experience is quite simple. Let's assume, for examle, you have an enumeration of "type" in your table, and type translates to "square", "circle" and "triangle." You wish to enhance the control by providing a little more information about the item that was selected. You can easily create your custom control, then sprinkle in your customizations. Consider, for example, a Type_Edit.ascx markup that looks like this:

<asp:DropDownList ID="ddType" runat="server" CssClass="droplist">
    <asp:ListItem Value="0" Text="Square" />
    <asp:ListItem Value="1" Text="Circle" />
    <asp:ListItem Value="2" Text="Rectangle" />
</asp:DropDownList> <asp:Label ID="lblType" runat="server" />

Beneath that, we add a JavaScript block (using JQuery):


    var hints = new Array('All sides are the same length!', 
         'Circles have an intimate relatinship with pi',
         'A rectangle is a shape where parallel sides are equal length');

    $(document).ready(function() {
        $('#<%= ddType.ClientID %>').change(function() {
            setTypeHint();
        });
        $('#<%= ddType.ClientID %>').val('<%# FieldValueEditString %>');
        setTypeHint();
    });

    function setTypeHint() {
        var value = $('#<%= ddType.ClientID %>').val();
        $('#<%= lblType.ClientID %>').text(hints[value]);
    }

Now you can easily see the new "hint" every time the drop down field changes.

Changing Field Names

Changing the label or column heading that appears for a given column is as simple as changing the display name for a table. On the property you wish to provide a hint for, simply add the DisplayName attribute:

public class MyTableMetaData 
{
   [DisplayName("My Field")]
   public object MyField { get; set; }
}

Swapping out Pages

Sometimes you may want to override the built-in functionality for a given page. For example, on a grid list, you might want to restrict the columns that are displayed. Definining your own page in place of the built-in template is simple.

  1. Under the DynamicData folder, create a new folder called CustomPages
  2. Under CustomPages, create a new folder with the same name as the class you want to change the page for. For example, if your class is called MyTables, you will create a folder called MyTables
  3. Add the page you wish to override. In our example, we'll copy the List.aspx page from the PageTemplates folder and paste it into the MyTables folder. Now, when the list for MyTables is displayed, it will use the new page instead of the supplied template.

In the grid definition, I can add the asp:DynamicField tag for any fields I wish to display, and leave out the ones I don't want (or I might even decide to use something besides a grid altogether)

The Foreign Key Edit Bug

If your table has a many-to-many relationship, you may have observed a bug. Let's say we have Groups and Persons, and a person can belong to multiple groups. In the "Group" display, there will be a link generated for "View Persons" that takes me to the list of persons associated to the that group. If I click "edit" however, suddenly the class name for the LINQ entity appears intead of a nice link!

This bug is easy to fix. You don't want the user editing the associations "in line", so simply copy the Children.ascx field template and paste it as Children_Edit.ascx. This will cause the same "view link" code to fire in edit mode, and allow the user to navigate to the associations and edit them directly, rather than being presented with the result of a ToString that was never overridden.

Complex Validation (LINQ Flavor)

In the many-to-many example, it is often a requirement that only one unique relationship exists (for example, you do not want to have multiple instances of "Person A belongs to Group 1" in your many-to-many link table). This may be enforced by a database rule. However, when you fire up your dynamic data application and test adding a duplicate link, you simply see the yellow exclamation mark indicating a JavaScript error and see that a unique index constraint was violated. This isn't very user friendly!

Fortunately, the LINQ class provides a hook to perform your own validation, called OnValidate. For this particular example, I wrote a static class to extend my data access validations, that looked like this:

public static class PersonGroupLinkExtensions
{
    // check for a duplicate prior to inserting
    public static bool IsDuplicate(int personId, int groupId)
    {
        using (MyDatabaseDataContext context = new MyDatabaseDataContext())
        {
            int count = (from link in context.PersonGroupLinks
                         where link.PersonID == personId
                         && link.GroupID == groupId
                         select link).Count();
            return count > 0;
        }
    }
}

Simple enough: simply validate whether or not that combination already exists. Then, in the partial class I used to extend from the LINQ class, I implement the partial method OnValidate: (partial methods are different than overrides ... good homework project if you are not familiar with them).

partial void OnValidate(System.Data.Linq.ChangeAction action)
{
    if (action.Equals(System.Data.Linq.ChangeAction.Insert))
    {
        if (PersonGroupLinkExtensions.IsDuplicate(this.PersonID, this.GroupID))
        {
            throw new ValidationException("Combination already exists and duplicates are not allowed.");
        }
    }
}

Conclusion

This is by no means an exhaustive coverage of dynamic data. There is much more to explore, from scaffolding to routes and the Entity Framework. Hopefully this is a good guide to help you get started, jump over a few hurdles many people encounter, and also discover just how flexible and rich the tools provided by dynamic data truly are.

Jeremy Likness

2 comments:

  1. Hi, thank you for the article.
    I want to Change the Field Names of an Auto-generated Type that is the result table of a stored procedure. Applying the DisplayName attribute doesn't works for me. Any ideas?

    ReplyDelete
  2. Hmmm, I didn't realize a stored procedure result would be handled any differently from a regular table. If I find what you're looking for, I'll be sure to post it!

    ReplyDelete