Chapter 11. Building Dynamic Forms

In This Chapter

Basic Form Layout

Configuring Appearance of Labels

Configuring Field Templates

Configuring Appearance of Values

Enum Properties and Enumeration Templates

Custom Data Types and UI Hints

Specifying Field Template Parameters

Field Template Interaction in Dynamic Forms

Creating Additional Dynamic Entity Templates

The biggest advantage Dynamic Data offers is the dramatic reduction in the amount of code in your application. Consider the number of ASPX files a web application would need to have for the Northwind Traders sample database. If you were using WebForms, you could manually create two ASPX files per entity—one multipurpose “form” page for inserting, editing, displaying, and deleting single rows, plus another “list” page for searching for and browsing through all rows. With 11 distinct entity types in the Northwind database, this means that 22 separate ASPX pages would have to be created. With ASP.NET MVC, you typically have separate single-purpose views, requiring five ASPX files per entity, increasing the total number of ASPX files to to 55, more than doubling the number of hand-written ASPX pages with WebForms. With Dynamic Data, which generates code at run time, the application can be built with only four ASPX files (page templates), regardless of the number of entity types.

Dynamic Data gives you a working application as soon as the initial version of the entity/data model is available. You can immediately begin sharing it with the business and collecting feedback on functioning web pages as opposed to UML diagrams or screen mockups. With a good set of dynamic templates, you can refine the entity model and solicit additional feedback in rapid successive iterations without the penalty of maintaining the presentation logic. If a particular entity requires a custom user interface that cannot be generated dynamically, you can hand-code it using either the WebForms or the MVC framework, when the entity model is stable.

By helping to postpone the hand-coding of the user interface and making the application size smaller, Dynamic Data helps your project stay agile, reduces waste, and enables you to focus on the most important aspect of the information system—its domain model. After all, the biggest risk on any development project is building the wrong thing. If your domain model is wrong, your application will be wrong, no matter how pretty it looks. This is the most important reason you might want to master the metadata-driven development of web applications with Dynamic Data. This chapter focuses on how to do this effectively, points out some of the pitfalls and shortcomings you might encounter, and discusses how to work around them.

Basic Form Layout

When creating custom forms, you control their layout by explicitly creating table or other HTML elements. You determine which entity properties appear on the form and where they are displayed by creating a DynamicControl for each property and placing it in the required location.

With the default entity template, Dynamic Data generates a single-column form for any entity type, with labels on the left side and data entry fields on the right side. Unless you specify otherwise, Dynamic Data uses a heuristic algorithm to display all appropriate properties it supports. In particular, this list does not include the generated, custom, and foreign key properties. Generated properties, such as properties mapped to identity columns in a Microsoft SQL Server database, typically contain information not suitable for display to end users. Custom properties, such as a FullName property that could be defined in a contact entity class to concatenate the FirstName, LastName, and MiddleInitial properties, cannot be used in LINQ filters with Entity Framework and would break list pages. Foreign key properties, such as a CustomerId, are not only inappropriate for users to see or edit directly, they are a part of navigation properties, such as Customer, which are already displayed, usually with a ForeignKey field template.

If this default set of properties selected for display by Dynamic Data does not meet your requirements, you can explicitly show or hide a particular property with the help of the DisplayAttribute. The code snippet next illustrates how to show the EmployeeID and hide the Orders properties of the Employee entity:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [Display(AutoGenerateField = true)]
    public object EmployeeID { get; set; }

    [Display(AutoGenerateField = false)]
    public object Orders { get; set; }
  }
}

The AutoGenerateField property affects both single-item forms and list pages generated by Dynamic Data. For list pages, you can also set the AutoGenerateFilter property in the DisplayAttribute to explicitly specify whether the QueryableFilterRepeater control will generate a DynamicFilter control instance for this property.


Note

DisplayAttribute is the main data annotation attribute you use to control the layout and appearance of dynamic forms in version 4.0 or later of ASP.NET Dynamic Data. It was designed to replace several other data annotation attributes, including DisplayNameAttribute, DescriptionAttribute, ScaffoldColumnAttribute, and ReadOnlyAttribute, that were used by the original version of Dynamic Data released with ASP.NET 3.5 SP1. These attributes are still supported; however, with rare exceptions, you should consider using the DisplayAttribute as it combines multiple annotations into a single, easy-to-discover attribute class.


By default, properties appear on a dynamic form in the order they are defined in the entity model. If this does not match what you need, ideally you want to change the order of properties in the entity model. This is easy to do if you are using the code-first approach with Entity Framework. If you are using the model-first approach, you can do this by editing the raw XML in the EDMX file in a text editor. If you are using the database-first approach, you need to reorder columns in the database table and regenerate the model. Controlling order of data entry controls on dynamic forms by reordering properties in the entity model is ideal because it eliminates ambiguity and keeps the model simple. After all, if you had a custom form laid out with an HTML table, you would want to move a particular row to its new location, keeping the order of elements in the markup in sync with their appearance on screen. Same idea applies to controlling order of controls in dynamically generated forms.

However, in the real world, you have to deal with the less than ideal tools and larger than ideal databases. When changing the order of properties in the entity model becomes impractical, you can specify it explicitly by setting the Order property of the DisplayAttribute. Here is an example that shows how to do this for the FirstName and LastName properties of the Employee entity:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [Display(Order = 110)]
    public object FirstName { get; set; }

    [Display(Order = 120)]
    public object LastName { get; set; }
  }
}

When specifying order numbers explicitly, give yourself a little room by starting with appropriate gaps between neighboring properties. In the example just shown, the FirstName and LastName properties that sit next to each other have a gap of 10 in their order numbers. If you need to insert another property, perhaps the MiddleInitial, between them in the future, you will be able to do it without having to renumber all properties. Start with a gap of 100 for larger entities that have 20 or more properties. This allows you to move entire groups of properties without renumbering. When the entity design stabilizes, you can either renumber the properties to have a uniform gap or create a custom entity template and remove order numbers from the metadata entirely.

All properties with an explicit display order are displayed first. Properties that do not have DisplayAttribute or don’t have the Order property specified are displayed at the end of the list. This logic is currently implemented by giving properties a default order of 10,000. Keep this in mind when working with large entities—you don’t want your explicit order numbers to exceed this number.

Even though gaps allow you to change the order of properties on dynamic pages without having to renumber all of them, it is still a good idea to have the property order in the actual source file match the order of their order numbers. This makes it easier to understand how the dynamic page will look and make maintaining order numbers simpler.

Configuring Appearance of Labels

In custom forms, the text elements and labels that appear on pages are specified explicitly in their markup. In dynamically generated pages, Dynamic Data uses the internal name of entities and their properties by default. When the internal name does not match what you want to appear on screen, your first choice should be to adjust the internal name in the entity model. For instance, if your entity model has a Customer entity, such as the one from the Northwind Traders sample shown in Figure 11.1, you might have properties such as ContactName and CompanyName that use PascalCase or another naming convention to concatenate multiple words into a single identifier.

Image

Figure 11.1. Customer entity in Entity Designer.

Although business users can easily understand the term ContactName, it is not a proper English word; it would be unprofessional to show it in the application. You already know from the previous chapters in this book that you can easily extend metadata information to specify that Name is the proper display name for the ContactName property. However, before you add a DisplayAttribute to the model, take this opportunity to stop and think about why the internal property name does not match its display name. This can be a sign of a mismatch between the domain terminology of your business users and the internal technical terminology you use in the implementation of the app. In this example, you might speculate that you cannot use the term Name because you want to distinguish between the name of the person and the name of his or her company: Thus, technical terms ContactName and CompanyName were chosen to distinguish them. But why is there ambiguity here in the first place? Does your company, Northwind Traders, serve consumers or businesses? If you are a consumer-focused company, you can reinforce this fact in your entity model by renaming the ContactName property to simply Name and changing the CompanyName property to simply Company, making it one of the address properties.

It is very important to keep internal entity and property names accurate. When names displayed on screen do not match expectations of your business users, the first choice should be to change the entity model to match terminology of your business domain. Ideally, you want to propagate these name changes all the way down to the database level and have the names of tables and columns consistent with the names of entities and properties. Refactoring tools in Visual Studio make this task possible today, even with significant amounts of .NET and SQL code. Getting in the habit of applying domain terminology throughout the application helps to reduce the cost of ongoing maintenance and helps the entire development team, including the new members, to understand the application better.

When the internal name of a property is consistent with the business terminology but still not appropriate for display to users, you can use data annotations to specify proper names that should be used instead. Of course, you might also have to resort to this approach when renaming properties in your entity model and columns in your database becomes too difficult. You can start by setting the Name property of the DisplayAttribute—Dynamic Data uses this value whenever a display name is required for the entity property, including form labels, grid column headers, and validation error messages. If a single display name is not enough, you can also specify a separate value in the ShortName property of the DisplayAttribute, which is automatically used in grid column headers. The code snippet that follows shows an example of specifying a display name and a short name for the HomePhone property of the Employee entity in the Northwind sample:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [Display(Name = "Home Phone", ShortName = "Home #")]
    public object HomePhone { get; set; }
  }
}


Note

It is not necessary to specify both ShortName and Name properties of the DisplayAttribute if they have the same value. If the ShortName is not specified, Dynamic Data automatically uses the Name instead.


Prompt Annotation

The DisplayAttribute includes several additional properties that can be used to further tune the appearance of the entity property in dynamically generated pages, although not all of them are supported by the Dynamic Data out of the box. In particular, the Prompt property of the DisplayAttribute could be used to provide a custom string for use in form labels, similar to how the ShortName property specifies display name for grid column headers. To implement this, you can simply change the dynamic entity template, Default.ascx, to use the prompt string if it was specified and otherwise fall back to using the display name string, as shown next:

protected void Label_Init(object sender, EventArgs e)
{
  var label = (Label)sender;
  label.Text = this.currentColumn.Prompt
      ??
this.currentColumn.DisplayName;
}


Note

Here and in the rest of this chapter, assume dynamic entity templates Default.ascx, Default_Edit.ascx, and Default_Insert.ascx have been combined into a single, multimode template, Default.ascx, as discussed in Chapter 10, “Building Custom Forms.”


This code first appeared in Listing 10.6 and was discussed in detail in Chapter 10. The Label_Init method handles the Init event of the Label control the entity template generates for each entity property. The currentColumn field contains a reference to the MetaColumn object describing the current entity property for which a label and a data entry control are being generated. The original version of this code initialized the label Text using only the DisplayName property of the MetaColumn object. The new version just shown first tries to initialize the label text using the Prompt before falling back to the DisplayName.


Note

As discussed in Chapter 7, “Metadata API,” the MetaColumn class encapsulates data annotation attributes applied to the entity property. Although it allows accessing data annotation attributes, using its Attributes property, it also provides strongly typed properties to access specific annotations directly. For example, the DisplayName and Prompt properties of the MetaColumn class return values specified in the Name and Prompt properties of the DisplayAttribute, respectively. Using these properties is not only easier, it is also faster due to metadata caching performed by Dynamic Data.



Note

The ?? operator is a C# null-coalescing operator. It’s a powerful shortcut but can be confusing if you are not familiar with it. Here is how a single line of code from the snippet just shown can be replaced with a more verbose, but straightforward, if statement:

if (this.currentColumn.Prompt != null)
  label.Text = this.currentColumn.Prompt;
else
  label.Text = this.currentColumn.DisplayName;


After implementing support for the Prompt annotation in the dynamic entity template, you can further refine the definition of the Employee entity and specify a custom string for use with the HomePhone property in form labels as shown next. Also the RequiredAttribute is added to the HomePhone property to help illustrate the fact that the Name annotation is still important because it is used to generate validation error messages.

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [Required]
    [Display(Name = "Home Phone", ShortName = "Home #",
      Prompt = "Home Phone Number:")]
    public object HomePhone { get; set; }
  }
}

Notice how the Name, ShortName, and Prompt annotations look in a dynamically generated UI in Figure 11.2. At the top of the picture, you can see a fragment of a dynamic form where the Prompt annotation appears as a label to the left of the phone number text box. At the bottom of the picture, a fragment of a dynamic grid shows the home phone column where the ShortName annotation appears in the column header. On the right side, you can see the validation error message generated by the RequiredAttribute, which includes the Name annotation.

Image

Figure 11.2. Name, ShortName, and Prompt annotations in dynamic UI.

Description Annotation

The DisplayAttribute also defines Description property, which most of the built-in field templates supplied by Dynamic Data use to provide a tooltip for their data controls. The Description annotation offers a good way to store documentation describing the entity properties during initial development of the entity model. During active development, descriptions in code help upi remember the intent of different properties; the tooltips in the user interface make discussing them with the business users easier as well. When the entity model design is stable, you refine the descriptions to serve as interactive hints for users during data entry. The code snippet that follows illustrates the last scenario; you can see how it looks in Figure 11.3:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [Display(Prompt = "Home Phone Number:",
      Description = "Please enter a 10-digit Phone Number")]
    public object HomePhone { get; set; }
  }
}

Image

Figure 11.3. Description annotation in Dynamic UI.

In field templates, this annotation is available via the Description property of the MetaColumn class. Field templates typically use it in Load event handlers to set the ToolTip property of their data control, such as a TextBox or a DropDownList. The Boolean_Edit, ForeignKey_Edit, and ManyToMany_Edit field templates do not support the Description annotation out of the box. However, it is easy enough to fix this problem by adding code similar to the following snippet. This example was taken from the Boolean_Edit field template in the source code accompanying this book:

protected void Page_Load(object sender, EventArgs e)
{
  this.checkBox.ToolTip = this.Column.Description;
}


Note

By design, the DisplayAttribute cannot be applied to classes; it contains many properties that simply would not be applicable. To provide a human-readable name for an entity type, you need to use the DisplayNameAttribute, as shown in here:

[DisplayName("Order Items")
partial class Order_Detail
{
}


Configuring Field Templates

After specifying which properties of an entity should appear on a dynamic form, in what order they should appear, and what labels they should display, you might also need to specify which field templates will be used for data entry and how they should be configured.

Making Properties Read-Only

One of the important decisions to make when designing entities in your application is whether a particular property can be modified by the user directly or supplied by the system and displayed to the user in Read-only mode. In a custom page or template, you can specify whether an Edit mode or a Read-only field template will be loaded by setting the Mode property of the DynamicControl or DynamicField explicitly in markup:

<asp:DynamicControl runat="server" DataField="SubmittedDate" Mode="ReadOnly"/>

When the DynamicControl instances are generated dynamically, the field template mode needs to come from the model. If you were using a code-first or model-first approach in your entity model, your first choice should be making that property Read-only in the entity model. Here is an example of doing this with a code-first approach:

public DateTime SubmittedDate { get; internal set; }

Notice that the property setter has internal visibility, which, assuming your entity model is in a separate project, will make it read-only in the Dynamic Data web application. With the model-first approach, you can change property visibility in the Entity Designer and have it generate the property with an internal setter.

Although it is also possible to use the Entity Designer to change property visibility with the database-first approach, you might want to avoid doing this because customization made to the generated entity model by hand quickly becomes difficult to maintain. With the database-first approach, when the EDMX is regenerated every time the database schema changes, your best option is to apply the EditableAttribute to the property in the metadata class. Here is how you can do this for the Order entity:

[MetadataType(typeof(Metadata)]
partial class Order
{
  abstract class Metadata
  {
    [Editable(false)]
    public object SubmittedDate { get; set; }
  }
}

Whether you make a particular property read-only in the entity model or use the EditableAttribute, Dynamic Data (or more specifically the DynamicControl) uses the metadata information to load a read-only version of the field template, even if the mode of the form itself is Edit or Insert.

The EditableAttribute can be also used to selectively change whether a particular property can be edited in the Insert mode. For instance, if you wanted to allow your customers to enter the RequiredDate for their orders only when submitting the order but prevent them from changing it later, you could specify false as the AllowEdit property of the EditableAttribute, but true as its AllowInitialValue property. Here is an example:

[MetadataType(typeof(Metadata)]
partial class Order
{
  abstract class Metadata
  {
    [Editable(false, AllowInitialValue = true)]
    public object RequiredDate { get; set; }
  }
}

Overriding Default Field Templates

Dynamic Data chooses an appropriate field template for each property based on its type. If you find yourself in a situation where you need to change the field template Dynamic Data selected, it is best to stop and ask yourself if the property has an appropriate type. For instance, if you have a string property and want to force Dynamic Data to use an integer field template, there is a good chance that the property type is incorrect. The best course of action in that situation would be to correct the problem at the source—in the entity model and the database.

If the property does have an appropriate type, but the .NET type is not specific enough for your needs, your next choice is to apply the DataTypeAttribute to the property in the metadata class. For example, the HomePhone property of the Employee entity is of type string; however, it is very different from another string property, Notes. The first stores phone numbers; the second stores one or more lines of text. Here is how you can clarify this distinction in the entity model by applying the DataTypeAttribute:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [DataType(DataType.PhoneNumber)]
    public object HomePhone { get; set; }

    [DataType(DataType.MultilineText)]
    public object Notes { get; set; }
  }
}

By applying the DataTypeAttribute, you are giving Dynamic Data a more specific type it can use to choose a more appropriate field template, such as the MultilineText_Edit field template that comes with Dynamic Data out of the box and the PhoneNumber_Edit field template created in Chapter 3, “Field Templates.”


Note

Applying the DataTypeAttribute to specify MultilineText type for a string property should not be necessary in many circumstances. Dynamic Data is smart enough to automatically recognize properties mapped to database columns of type VARCHAR(MAX) or TEXT as MultilineText. Therefore, treat it with suspicion and remember that it is better to address a mismatch by changing the entity model instead of applying a data annotation attribute.


Configuring Appearance of Values

When building custom forms with Dynamic Data components, you can explicitly control appearance of values by specifying the DataFormatString, NullDisplayText, and HtmlEncode properties of the DynamicControl and DynamicField controls directly in markup. With the TextBox-based field templates, you can also control appearance of values in Edit and Insert mode by setting the ApplyFormatInEditMode and ConvertEmptyStringToNull properties. Here is an example used in Chapter 3, which covers this topic in detail, to display the ShippedDate property of the Order entity in MM/dd/yy format:

<asp:DynamicField DataField="ShippedDate" DataFormatString="{0:MM/dd/yy}" />

You can also specify these values in the metadata, by applying the DisplayFormatAttribute to entity properties as shown here:

[MetadataType(typeof(Metadata)]
partial class Order
{
  abstract class Metadata
  {
    [DisplayFormat(DataFormatString = "{0:MM/dd/yy}")]
    public object SubmittedDate { get; set; }
  }
}


Note

Under the hood, the DynamicControl uses properties of the DisplayFormatAttribute applied to the column to initialize its own. In other words, if you have a DataFormatString annotation specified in the metadata and in markup, the value specified in markup takes precedence, as you would expect.


If you need values of a particular property to appear consistently on multiple pages of your web application, the DisplayFormatAttribute works great because it allows you to specify the format string in a single place. However, because this logic falls in to the presentation category, you should use this attribute with caution. You can often avoid using the DisplayFormatAttribute and embedding the presentation logic in the business layer by using the DataTypeAttribute instead. For Date, Time, and Currency data types, this attribute provides default format strings, which are sufficient in many cases. Here is how you could modify metadata of the SubmittedDate property to use the DataType annotation instead:

[MetadataType(typeof(Metadata)]
partial class Order
{
  abstract class Metadata
  {
    [DataType(DataType.Date)]
    public object SubmittedDate { get; set; }
  }
}

For DataType.Date, the DataTypeAttribute provides a default format string {0:d}, which with English (United States) regional settings is equivalent to the custom format string {0:M/d/yyyy}. If you can live with a four-digit year format for the SubmittedDate, using the DataTypeAttribute instead of the DisplayFormatAttribute is better. It is not only simpler, but also keeps the presentation logic out of the entity model and keeps the metadata at the (higher) business level. Otherwise, using the DisplayFormatAttribute might still be preferrable to embedding this information in multiple custom pages throughout the application.

Enum Properties and Enumeration Templates

In entity design, you often come across the need to have a property with a fixed list of possible values. A common way to present such properties to users is a drop-down list or a group of radio-buttons. Discovering a need for such a control is another opportunity for you to stop and carefully consider how this information should be presented in the entity model.

If the list of possible values for a property will grow over time and the application will not need any special logic for each new value, it is best to make this property a foreign key and store the values in a separate table/entity. Dynamic Data automatically uses the ForeignKey field templates for foreign key properties, and the users see a drop-down list populated with values from the lookup table.

On the other hand, if the list of possible values for a property is fixed and especially if the application will have different business rules for different values, it is best to define the list of values as an enumerated type, such as the EmployeeType enum shown in the code in this example:

public enum EmployeeType: byte
{
  Fulltime = 0,
  PartTime = 1,
  Temporary = 2
}

Because the Entity Framework does not support enumerated types in .NET version 4, you also have to rely on the EnumDataTypeAttribute to associate the enum type with an integral entity property as shown here:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
      [EnumDataType(typeof(EmployeeType))]
      public object EmployeeType { get; set; }
  }
}

As you recall from Chapter 3, Dynamic Data automatically uses the Enumeration field templates for all properties that have the EnumDataTypeAttribute applied. In Edit mode, the enum items are displayed in a DropDownList control that looks identical to the drop-down list produced by the foreign key template.

There is a wide gray area between these two extremes, and you will often need to make a judgment call on whether a particular property should be an enum or a foreign key. On one hand, using a lookup table and a foreign key is more flexible—you could add new values without recompiling the application—and implementing reports at the database level is easier. On the other hand, using enum properties is much simpler, especially when you need to have business rules attached to different values.

When in doubt, it is best to start with the simplest solution and change it when you have a better understanding of the business domain. With this in mind, start with an enum because this approach reduces the number of entities and relationships in your entity model as well as the number of tables and foreign keys in your database. It also simplifies the application code because there is no need to load lookup values from a table or define special constants. Later, if you discover that business users will want to add new values to the list regularly, you can create a lookup entity and introduce a foreign key then.

Extending the Enumeration Field Templates

Unfortunately, Dynamic Data web applications do not support display name annotations for enum items out of the box. For instance, if your enum items need to have compound names, such as PartTime, the built-in Enumeration and Enumeration_Edit field templates display the internal names to the users. Obviously, this looks unprofessional, and you might be tempted to convert the enum property to a foreign key and introduce a lookup table just to store the display names. Luckily, it is easy to solve this problem by extending the built-in templates and taking advantage of the data annotation attributes. This is a small, one-time investment of effort that will provide ongoing savings in cost and complexity over the lifetime of your application.

To specify display names for enum items, you can either create a new or reuse an existing data annotation attribute. As it turns out, the DisplayAttribute can be applied not only to entity properties, but to enum items as well. The following code shows how to modify the definition of the EmployeeType enum to specify display names for all its values:

public enum EmployeeType: byte
{
  [Display(Name = "Full-time")] Fulltime = 0,
  [Display(Name = "Part-time")] PartTime = 1,
  [Display(Name = "Temporary")] Temporary = 2
}


Note

Reusing an existing .NET framework type is always better when it fits your needs because it helps keep the solution smaller and simpler. In this example, reusing the DisplayAttribute to provide display names for enum items offers not only consistency with entity properties, but also allows you to take advantage of the built-in localization support and use additional display name properties, such as the ShortName and Description. However, always be cognizant of the side effects from reusing a type for an unintended purpose. The AutoGenerateField and AutoGenerateFilter properties of DisplayAttribute will not be applicable to enum items, possibly causing confusion for other developers. In this scenario, the benefits of reuse and consistency outweigh the added complexity of creating a new attribute. However, use your own judgment to make similar tradeoffs in your projects.


Having decided on how display names of enum items will be specified in the model, you now need to modify the Enumeration.ascx and Enumeration_Edit.ascx field templates to access the additional metadata. To make this task easier, as well as to enable reuse of this logic in additional field templates (you might need another field template that displays a list of radio-buttons instead of a drop-down list), the sample solution implements it in a new base class with the two enumeration field templates to inheriting from it.

Listing 11.1 shows the complete source code of the new base class, called UnleashedEnumerationFieldTemplate to follow the naming convention established in this book. This class inherits from the UnleashedFieldTemplate (a class introduced in Chapter 10) and overrides the FieldValueString property and the PopulateListControl method inherited from the ultimate base class of field templates—the FieldTemplateUserControl provided by Dynamic Data.

As you are reviewing the implementation of this class, notice that the BuildEnumDictionary method is responsible for collecting the metadata information describing a given enum type. It uses reflection API to examine each item defined in the enum type, determine its display name, and store the enum names and values in an OrderedDictionary object. This specialized collection class was chosen because, on one hand, you need a dictionary to support fast lookup of a display name based on an enum value. On the other hand, you want to preserve the original order of enum items when showing their display names in a drop-down list. The OrderedDictionary class from the System.Collections.Specialized namespace is the only dictionary class provided by the .NET framework that also preserves the order of its items.

The GetEnumDictionary method is responsible for calling the BuildEnumDictionary method to extract metadata the first time a particular enum type is processed by the application, caching the results to improve performance. This is a common and important pattern when working with metadata. You want to cache the extracted metadata because it requires multiple sequential loops and reflection calls to collect. You definitely want to avoid doing this unnecessarily as it will have a significant negative impact on overall performance of the web application. In a worst-case scenario, where you have a page with a GridView control that displays multiple enumeration columns and a large number of rows, the performance hit might be noticeable even with a single user accessing the system. By storing the extracted metadata in a static ConcurrentDictionary object, the UnleashedEnumeraterFieldTemplate ensures that any given enum time is processed only in the application’s lifetime, minimizing any performance impact of reflection.

Listing 11.1. UnleashedEnumerationFieldTemplate Base Class


using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Web.DynamicData;
using System.Web.UI.WebControls;

namespace Unleashed.DynamicData
{
  public class UnleashedEnumerationFieldTemplate : UnleashedFieldTemplate
  {
     private static readonly ConcurrentDictionary<Type, IOrderedDictionary>
      enumCache = new ConcurrentDictionary<Type, IOrderedDictionary>();

    public override string FieldValueString
    {
      get
      {
        object fieldValue = this.FieldValue;
        Type enumType = this.Column.GetEnumType();
        if (fieldValue != null && enumType != null)
        {
          fieldValue = Enum.ToObject(enumType, fieldValue);
          IOrderedDictionary enumDictionary = GetEnumDictionary(enumType);
          var enumName = (string)enumDictionary[fieldValue];
          if (!string.IsNullOrEmpty(enumName))
            return enumName;
        }

        return base.FieldValueString;
      }
    }

    protected new void PopulateListControl(ListControl listControl)
    {
      Type enumType = this.Column.GetEnumType();
      Type underlyingType = Enum.GetUnderlyingType(enumType);
      IOrderedDictionary enumDictionary = GetEnumDictionary(enumType);
      foreach (DictionaryEntry entry in enumDictionary)
       {
        var enumName = (string)entry.Value;
        object enumValue = Convert.ChangeType(entry.Key, underlyingType);
        listControl.Items.Add(new ListItem(enumName, enumValue.ToString()));
      }
    }

    private static IOrderedDictionary GetEnumDictionary(Type enumType)
    {
      IOrderedDictionary enumDictionary;
      if (!enumCache.TryGetValue(enumType, out enumDictionary))
      {
        enumDictionary = BuildEnumDictionary(enumType);
        enumCache.TryAdd(enumType, enumDictionary);
      }

      return enumDictionary;
    }

    private static IOrderedDictionary BuildEnumDictionary(Type enumType)
    {
      var dictionary = new OrderedDictionary();
      FieldInfo[] enumFields = enumType.GetFields(
        BindingFlags.Static ¦ BindingFlags.GetField ¦ BindingFlags.Public);
      foreach (FieldInfo enumField in enumFields)
      {
        object enumValue = enumField.GetValue(null);
        DisplayAttribute annotation = enumField
          .GetCustomAttributes(typeof(DisplayAttribute), false)
          .OfType<DisplayAttribute>().FirstOrDefault();
        string enumName = annotation != null
          ? annotation.GetName()
          : enumValue.ToString();
        dictionary.Add(enumValue, enumName);
      }

      return dictionary;
    }
  }
}


Implementation of the overridden FieldValueString property getter is straightforward. It calls the GetEnumDictionary method to retrieve the dictionary with display names and values of the enum items and uses it to find the display name that matches the current field value. However, it also has to support a couple of edge cases. One of them is when the field value is null and you have to call the inherited getter to properly handle formatting options for null values. The other is when the field value does not match any known enum item and you, again, call the inherited getter, which converts it to string based on the property’s formatting options.

The PopulateListControl method uses the new keyword because this method is not virtual in the base class, FieldTemplateUserControl. However, this should not be a problem because the method will not be called polymorphically. Consistently with its implementation in the base class, the PopulateListControl method also converts enum values to their underlying integral equivalents for use in the list items. So instead of a longer “PartTime”, the value of the enum item will be simply “1”. This reduces the size of HTML and post-back data used by the page.

To take advantage of the new base class, you need to modify the Enumeration.ascx.cs and the Enumeration_Edit.ascx.cs code-behind files that come with the enumeration field templates out of the box so that they inherit from UnleashedEnumerationFieldTemplate instead of the built-in FieldTemplateUserControl. Because PopulateListControl is a direct replacement for the inherited method, no further changes are needed in the Enumeration_Edit field template. It automatically starts showing the annotation-based display names as illustrated in Figure 11.4.

Image

Figure 11.4. Extended Enumeration_Edit Field template.

In addition to inheriting from the UnleashedEnumerationFieldTemplate base class, the Enumeration field template also needs to be modified to use the overridden FieldValueString instead of the EnumFieldValueString property that was defined in this template out of the box. Markup of the modified version of the Enumeration field template is shown in Listing 11.2.

Listing 11.2. Modified Enumeration Field Template (Markup)


<%@ Control Language="C#" CodeBehind="Enumeration.ascx.cs"
  Inherits="WebApplication.DynamicData.FieldTemplates.EnumerationField" %>
<asp:Literal runat="server" ID="literal" Text="<%# FieldValueString %>" />


Overall, the implementation of the Enumeration field template becomes much simpler because you are mostly removing unnecessary code. The new version of the code-behind is shown in Listing 11.3.

Listing 11.3. Modified Enumeration Field Template (Code-Behind)


using System.Web.UI;
using Unleashed.DynamicData;

namespace WebApplication.DynamicData.FieldTemplates
{

  public partial class EnumerationField : UnleashedEnumerationFieldTemplate
  {
    public override Control DataControl
    {
      get { return this.literal; }
    }
  }
}


Figure 11.5 shows how the modified version of the Enumeration field template looks on a page; as you can see, the display name of the EmployeeType is shown, not its internal (PascalCased) name.

Image

Figure 11.5. Extended Enumeration field template.

Enum types help to reduce complexity of entity models and improve performance of the application. This simple extension to the built-in Enumeration field templates to support display name annotations eliminates one of the most common reasons why developers introduce lookup tables in their applications. This allows you to take full advantage of enums today and prepare for the upcoming 4.5 release of the Entity Framework where you get first class support in both Entity Designer and LINQ to Entities.

Custom Data Types and UI Hints

The DataType enumeration does not define all possible business data types you might need in your entity models. For instance, the Region property of the Employee entity is of type string, a low-level technical type; however, from a business perspective, you could consider it to have a high-level type state, which limits its potential values to a list of about 50 states and territories used by the U.S. Postal Service. You can indicate this design in the entity model by applying the DataTypeAttribute to the Region property and specifying a custom type name instead of a predefined value from the DataType enumeration. Here is an example:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [DataType("State")]
    public object Region { get; set; }
  }
}


Note

As you might want to point out, you could also have created an enumerated type to represent the USPS list of states and territories. For the sake of this example, let’s continue with the assumption that using a string type makes more sense. As you see shortly, this is not a final design of the Region property anyway.


Dynamic Data treats custom data type names as first-class citizens. During field template lookup, it tries to find a template with the name matching the custom type specified in the DataTypeAttribute and falls back to the physical type only if it does not find the match. As it happens, a field template called State_Edit.ascx was created back in Chapter 10, which displays a drop-down list and allows users to select a valid state. Dynamic Data automatically uses it for the Region property of the Employee entity based on the data annotation in the preceding sample code.

Image

Figure 11.6. Web page generated by State field template.

When you encounter a property that fits one of the types defined in the DataType enumeration or perhaps a custom type unique to your business domain, it is a good idea to go ahead and use the DataTypeAttribute to indicate that decision in the entity model. In some cases, Dynamic Data will already have a built-in field template for this data type. When it does not, you can easily implement a new field template with a matching name, such as the PhoneNumber_Edit field template created in Chapter 3. By applying the DataTypeAttribute during entity design, you are documenting the business purpose of the property as well as some implied expectations of how values of this type will be presented to the users.

By applying the DataTypeAttribute to a property, you are saying that its type is fundamental to the business domain. For instance, the PhoneNumber and EmailAddress types are ubiquitous in contact management. However, saying that a particular type is fundamental is a strong statement, and discovering new types is difficult as it requires a solid understanding of the business domain. It is a good idea to postpone defining a new data type until you have at least two, ideally three or more, entity properties with a solid match for the emerging pattern.

What if you already know that a particular property needs a different UI but do not have any other examples that would justify definition of a new “data type?” In this situation, it is better to use the UIHintAttribute and specify the name of a custom field template instead of a data type. Going back to the example of Region property in the Employee entity, you could revise the premature decision to call “State” a data type and use the UIHint annotation instead:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [UIHint("State")]
    public object Region { get; set; }
  }
}

Although the value specified in both DataType("State") and UIHint("State") annotations is the same, the choice of attribute helps you to convey the design intent and distinguish between a distinct data type and a simple instance of a common data type that needs special user interface.


Note

Remember that by using the UIHintAttribute, you add presentation logic to the business layer of your application. Although placing business logic code in the code-behind files of a web application clearly is undesirable as it quickly leads to code duplication and makes automated testing difficult, combining presentation and business models is not necessarily a bad idea. You could argue that consistently using a UIHint instead of a DataType annotation helps to clarify your design intent in the model—a tangible benefit. However, every time you use the UIHint annotation, more and more of the presentation logic is mixed in with the business logic, making the model more complex. Use this as an opportunity to stop and consider whether creating a custom page or template for the given entity and separating the presentation logic from the business model would make the overall design simpler and easier to maintain.


Specifying Field Template Parameters

As discussed in Chapter 10, DynamicControl can initialize custom properties of field templates using matching attributes specified in markup. As an example, the built-in Text_Edit field template was extended with a custom Columns property that can be used to set the Columns property of the TextBox control it contains. If you had created a custom entity or page template for the Employee entity, you could make the Address text box wider and accommodate longer addresses by using the following markup:

<asp:DynamicControl runat="server" DataField="Address" Columns="65"/>

The UIHintAttribute also supports custom property values. They are exposed for consumption via its ControlParameters property, which returns an IDictionary<string, object>, and have to be specified as a sequence of names and values in the UIHintAttribute constructor, following the hint and presentation layer parameters. It is easier to explain this with an example; following is how you could specify the Columns parameter for the Address property in the metadata:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [UIHint("Text", "WebForms", "Columns", 65)]
    public object Address { get; set; }
  }
}

In this example, “Text” is the hint, the name of a field template; “WebForms” is the name of the presentation layer for which the UIHint is intended; and “Columns” is the name of the first control parameter followed by 65, its value. The UIHintAttribute constructor has an open-ended array as its last argument, so if you needed to specify additional control parameters, you would simply add more names and values to this list.


Note

Specifying “WebForms” as the presentation layer in the UIHintAttribute constructor is only required when you need separate UIHint annotations for different presentation frameworks, such as MVC and WebForms, for the same property. In that case, Dynamic Data picks the UIHint where the presentation layer is specified as “WebForms”. Otherwise, it uses the UIHint for “MVC” or the one where the presentation layer is not specified. If you are not planning to use UIHint annotations with multiple platforms, leaving it null might be the best option.


Unfortunately, the built-in DynamicControl does not support custom control parameters specified in the UIHintAttribute the way it supports its own custom attributes specified in markup. However, you can easily work around this limitation by extending the dynamic entity templates. As discussed in Chapter 4, “Entity Templates,” and later in Chapter 10, the dynamic entity templates rely on the Init event handler to initialize each DynamicControl it creates. Extending this event handler is the simplest way to implement support for custom parameters. Here is how you can extend the Init event handler implemented in Default.ascx.cs, the code-behind file of the combined entity template created in Chapter 10:

protected void DynamicControl_Init(object sender, EventArgs e)
{
  var dynamicControl = (DynamicControl)sender;
  dynamicControl.DataField = this.currentColumn.Name;
  dynamicControl.Mode = this.Mode;
  dynamicControl.ValidationGroup = this.ValidationGroup;

  var hint = this.currentColumn.Attributes.OfType<UIHintAttribute>()
    .FirstOrDefault();
  if (hint != null)
  {
    foreach (KeyValuePair<string, object> p in hint.ControlParameters)
      dynamicControl.SetAttribute(p.Key, p.Value.ToString());
  }
}

The new code (indicated by bold font) tries to find a UIHintAttribute applied to the current column (a MetaColumn object describing an entity property for which a field template needs to be created). If the attribute was found, it uses the names and values in the ControlParameters dictionary to set custom attributes for the DynamicControl. The DynamicControl, which has not created the field template yet, takes care of the rest and uses the custom attribute values to set the public properties defined by the field template class, like the Columns property defined by the Text_Edit field template. Figure 11.7 shows how the Address text box, dynamically configured by this code, is now much wider than text boxes for other string properties.

Image

Figure 11.7. Address TextBox with Columns parameter.

Extending Dynamic Data to Support Control Parameters

Although the current solution allows you to continue evolving the Employee form with metadata, at some point, you might need to create a custom entity or page template. In a custom template, having to specify the Init event handler for every DynamicControl instance becomes a nuisance you would rather avoid. You also have a problem with the dynamic list pages that display data with the help of GridView control because it does not offer an easy way to access the DynamicControl instances.

This section shows how the Dynamic Data controls can be extended to implement consistent support for metadata-driven control parameters.

Extending DynamicControl to Support Control Parameters

The UnleashedControl was introduced in Chapter 10 to encapsulate some of the functionality necessary to implement field template interaction. You can further extend it to support control parameters. Here is an extract from UnleashedControl.cs (without the rest of the implementation reviewed in Chapter 10, which is not relevant to this discussion):

protected override void OnInit(EventArgs e)
{
  // ...
  this.Init += this.InitializeControlParameters;
  base.OnInit(e);
  // ...
}

private void InitializeControlParameters(object sender, EventArgs args)
{
  MetaColumn column = this.Column ?? this.Table.GetColumn(this.DataField);
  var hint = column.Attributes.OfType<UIHintAttribute>().FirstOrDefault();
  if (hint != null)
  {
    foreach (KeyValuePair<string, object> p in hint.ControlParameters)
      this.SetAttribute(p.Key, p.Value.ToString());
  }
}

Notice that you cannot implement this logic directly in the overridden OnInit method of the UnleashedControl. On one hand, you do not know which entity property this control represents before the inherited OnInit method is called because the Init event handler in the dynamic entity template has not had a chance to initialize the DataField property yet. On the other hand, after the inherited OnInit method is finished, the field template has already been created, and calling the SetAttribute method will have no effect. To solve this problem, the custom attributes are set in the InitializeControlParameters method. This method is added as the last handler to the Init event, which fires at the appropriate moment—after the initialization code in the dynamic entity templates but before the field template is created.

Although implementing initialization of field template parameters in the UnleashedControl is a little more involved, it offers a better encapsulation and allows you to simplify the implementation of the default entity template. This becomes more important as you begin implementing additional dynamic entity templates later in this chapter. Consider the final version of the Default.ascx entity template in Listing 11.4.

Listing 11.4. Final Version of Default Entity Template (Markup)


<%@ Control Language="C#" CodeBehind="Default.ascx.cs"
  Inherits="WebApplication.DynamicData.EntityTemplates.DefaultEntityTemplate" %>
<table class="DDDetailsTable">
  <asp:EntityTemplate runat="server" ID="entityTemplate">
    <ItemTemplate>
      <tr>
        <td class="DDLightHeader">
          <unl:UnleashedLabel runat="server" OnInit="Label_Init" />
        </td>
        <td>
          <unl:UnleashedControl runat="server" OnInit="DynamicControl_Init" />
        </td>
      </tr>
    </ItemTemplate>
  </asp:EntityTemplate>
</table>


Because the UnleashedControl and UnleashedLabel (also introduced in Chapter 10) controls are designed to work with minimum configuration in both custom and dynamic entity templates, you only need to initialize their respective DataField properties with the name of the entity property for which the controls are being generated. The controls themselves take care of doing the rest, such as determining the display name of the property to use as a label, initializing the mode of the field template based on the mode of the entity template, and so on. Listing 11.5 shows the updated code-behind. Notice how the Label_Init and DynamicControl_Init methods shrink to a mere one line of code each, and the Label_PreRender method, responsible for associating labels with their data controls, disappears entirely.

Listing 11.5. Final Version of Default Entity Template (Code-Behind)


using System;
using System.Collections.Generic;
using System.Web.DynamicData;

namespace WebApplication.DynamicData.EntityTemplates
{
  public partial class DefaultEntityTemplate : EntityTemplateUserControl
  {
    private MetaColumn currentColumn;

    protected override void OnLoad(EventArgs e)
    {
      IEnumerable<MetaColumn> scaffoldColumns =
        this.Table.GetScaffoldColumns(this.Mode, this.ContainerType);
      foreach (MetaColumn column in scaffoldColumns)
      {
        this.currentColumn = column;
        var container = new NamingContainer();
        this.entityTemplate.ItemTemplate.InstantiateIn(container);
        this.entityTemplate.Controls.Add(container);
      }
    }

    protected void Label_Init(object sender, EventArgs e)
    {
      ((UnleashedLabel)sender).DataField = this.currentColumn.Name;
    }

    protected void DynamicControl_Init(object sender, EventArgs e)
    {
      ((UnleashedControl)sender).DataField = this.currentColumn.Name;
    }
  }
}


Extending DynamicField to Support Control Parameters

The updated UnleashedControl allows you to take advantage of metadata-driven control parameters in pages that rely on entity templates and template controls, such as the FormView or the ListView. However, you still have the same limitation in pages that rely on field controls, such as the GridView or the DetailsView. Listing 11.6 shows an example of such a page.

Listing 11.6. Page with GridView and DynamicField Controls (Markup)


<%@ Page Language="C#"
  MasterPageFile="~/Site.master" CodeBehind="SamplePage.aspx.cs"
  Inherits="WebApplication.Samples.Ch11.DynamicField.SamplePage" %>

<asp:Content ContentPlaceHolderID="main" runat="server">
  <asp:GridView ID="gridView" runat="server" DataSourceID="dataSource"
    AutoGenerateEditButton="true" AutoGenerateColumns="false"
    DataKeyNames="EmployeeID">
    <Columns>
      <asp:DynamicField DataField="FirstName" />
      <asp:DynamicField DataField="LastName" />
      <asp:DynamicField DataField="Address" />
      <asp:DynamicField DataField="City" />
      <asp:DynamicField DataField="Region" />
      <asp:DynamicField DataField="Country" />
      <asp:DynamicField DataField="PostalCode" />
    </Columns>
  </asp:GridView>
  <asp:EntityDataSource ID="dataSource" runat="server"
    ConnectionString="name=NorthwindEntities" EntitySetName="Employees"
    DefaultContainerName="NorthwindEntities" EnableUpdate="True" />
</asp:Content>


If you build and run this page, notice that the Address text box appears with its default size, ignoring the Columns parameter specified in the metadata to make it wider. This is because the DynamicField instances used to define grid columns create DynamicControl objects, which do not support control parameters defined in metadata. You can solve this problem by extending the DynamicField class to override its CreateDynamicControl method and return a new UnleashedControl instead of the built-in DynamicControl. Listing 11.7 shows how the UnleashedField class (available the sample solution) does that.

Listing 11.7. UnleashedField Implementation


using System.Web.DynamicData;

namespace Unleashed.DynamicData
{
  public class UnleashedField : DynamicField
  {
    protected override DynamicControl CreateDynamicControl()
    {
      return new UnleashedControl();
    }
  }
}


Extending DefaultAutoFieldGenerator to Support Control Parameters

With the UnleashedField class, you can fix the problem in the custom GridView page from Listing 11.6 by simply replacing the DynamicField instances with the extended controls. However, solving the problem in the dynamic page templates is a little more involved. As you recall from discussions in Chapter 6, “Page Templates,” a special Dynamic Data class, DefaultAutoFieldGenerator, is responsible for automatically generating the DynamicField instances for GridView and DetailsView controls based on a MetaTable object associated with it. Because the DefaultAutoFieldGenerator creates the DynamicField instances, you also need to extend it to have the UnleashedField instances created in dynamic page templates instead. Listing 11.8 shows the UnleashedFieldGenerator class, also available in the sample solution.

Listing 11.8. UnleashedFieldGenerator Implementation


using System.Web.DynamicData;
using System.Web.UI.WebControls;

namespace Unleashed.DynamicData
{
  public class UnleashedFieldGenerator: DefaultAutoFieldGenerator
  {
    public UnleashedFieldGenerator(MetaTable table): base(table)
    {
    }

    protected override DynamicField CreateField(MetaColumn column,
      ContainerType containerType, DataBoundControlMode mode)
    {
      var field = new UnleashedField();
      field.DataField = column.Name;
      if (containerType == ContainerType.List)
        field.HeaderText = column.ShortDisplayName;
      else
        field.HeaderText = column.Prompt ?? column.DisplayName;
      field.ItemStyle.Wrap = false;
      return field;
    }
  }
}


The UnleashedFieldGenerator provides a parameterized constructor, which is required to specify the MetaTable object when the generator is created. It also overrides the CreateField method to create and initialize a new instance of the UnleashedField control. In particular, it sets the HeaderText property of the new field using different values from the column metadata, depending on the context where the new field will be used. If List value was passed as the containerType, it means that the field will appear in a GridView control and the short version of column’s display name is used as the header. Otherwise, if Item container type was specified, the field will appear in a DetailsView control and the prompt (if it was specified) or the full display name of the column will be displayed in the label. Finally, wrapping is turned off to prevent validators from appearing on the next line if an error needs to be reported by the field template.

With the UnleashedFieldGenerator class, you still have to modify the List and ListDetails page templates to use it. In custom pages, where you initialize the ColumnsGenerator property of GridView control explicitly, you can simply change the code to instantiate the UnleashedFieldGenerator explicitly. However, in the dynamic page templates that come with the Dynamic Data project in Visual Studio, the DefaultAutoFieldGenerator is created by the DynamicDataManager during the InitComplete event of the Page. To replace this logic, you can create your own Page.InitComplete event handler as shown in this code snippet:

public partial class List : System.Web.UI.Page
{
  protected MetaTable table;
  protected void Page_InitComplete(object sender, EventArgs e)
  {
    this.gridView.ColumnsGenerator = new UnleashedFieldGenerator(this.table);
  }
}

Because the Page_InitComplete method runs after the event handler the DynamicDataManager uses to initialize the ColumnsGenerator property of the GridView, this code replaces it with the UnleashedFieldGenerator. As a result, the GridView will be populated with UnleashedField instances that have full support for metadata-driven control parameters, and the Address text box, for which the Columns parameter was specified in metadata of the Employee entity, will now be wide enough to support longer address values.

Field Template Interaction in Dynamic Forms

Is it possible to implement interaction between field templates in a dynamically generated web page? Yes. In fact you already have all the tools you need. Is it a good idea? Most of the time, it is not. When you discover that two or more data entry controls on a form need to interact with each other, the best course of action is to create a custom entity or page template, where implementing the interaction will be easier and more straightforward. Chapter 10, “Building Custom Forms,” discusses how to accomplish this task in detail.

However, you might discover recurring patterns in your entity model, where different properties need the same type of interaction. In Chapter 10, a custom entity template was created for the Customer entity to help users enter address information by suggesting a list of regions based on the name of the country they entered. In the Northwind sample model, several other entities include address properties, including Customer, Employee, Supplier, and Order. After implementing the same pattern several times for different entities, you might decide that a single, dynamic implementation would be beneficial.


Note

Remember that falling into the trap of creating a dynamic solution where custom code would be more appropriate is very common. Dynamic is not better than Custom. Don’t try to implement a dynamic solution based on just one or two instances of a particular pattern. With such a small number of cases, you will not have a sufficient understanding of the pattern and will not recoup the additional effort required to implement a dynamic solution. Even such a common pattern as the Region/Country properties could have been efficiently implemented with custom code, such as the generic Address entity template discussed in Chapter 4.


The key to implementing field template interaction with metadata is to place the logic of how to interact in the field templates and storing the information of who to interact with in the metadata. As an example, the Region field template created in Chapter 10 can be modified to provide an auto-completion list for users based on the value in the Country field template. However, because the country property can have different names in different entities—it is called ShipCountry in the Order entity—you do not want to hard-code it in the Region field template itself. Instead, the Country property name can be specified as a control parameter in the UIHintAttribute for the Region property, as shown in this code snippet:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [UIHint("Region", "WebForms", "CountryField", "Country")]
    public object Region { get; set; }
  }
}

Listing 11.9 shows the complete code-behind file of the Region_Edit field template, updated to support the CountryField parameter. Notice that the template class now has a matching public property, which is automatically initialized by the UnleashedControl based on the property metadata.

Listing 11.9. Region_Edit Field Template Extended with CountryField Parameter


using System;
using System.Collections.Specialized;
using System.Web.UI;

namespace WebApplication.DynamicData.FieldTemplates
{
  public partial class RegionEditField : UnleashedFieldTemplate
  {
    private UnleashedFieldTemplate countryTemplate;

    public string Country
    {
      get { return this.autoCompleteExtender.ContextKey;  }
      set { this.autoCompleteExtender.ContextKey = value; }
    }

    public string CountryField { get; set; }

    public override Control DataControl
    {
      get { return this.textBox; }
    }

    protected override void OnInit(EventArgs e)
    {
      base.OnInit(e);
      this.textBox.ToolTip = this.Column.Description;
      this.SetUpValidator(this.requiredFieldValidator);
      this.SetUpValidator(this.dynamicValidator);
    }

    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);

      this.countryTemplate = this.FindOtherFieldTemplate(this.CountryField);
      if (this.countryTemplate != null)
      {
        this.countryTemplate.AutoPostBack = true;
        this.countryTemplate.DataBinding += this.CountryFieldValueChanged;
        this.countryTemplate.FieldValueChanged += this.CountryFieldValueChanged;
      }
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
      dictionary[this.Column.Name] = this.ConvertEditedValue(this.textBox.Text);
    }

    protected void TextBox_TextChanged(object sender, EventArgs e)
    {
      this.OnFieldValueChanged(e);
    }

    private void CountryFieldValueChanged(object sender, EventArgs args)
    {
      this.Country = this.countryTemplate.FieldValueEditString;
    }
  }
}


The overridden OnLoad method passes the CountryField property value to the FindOtherFieldTemplate method provided by the base class, FieldTemplateUserControl, to find the actual field template that was created for the Country property on the page. If the country field template was found, its AutoPostBack property is set to true to make sure that a post-back occurs when the user changes the country value and can update the auto-completion list accordingly. The code also adds event handlers for the DataBinding and FieldValueChanged events of the country template.

The DataBinding event fires during initial rendering of the page, when the initial values of the country and region properties are loaded from the database. The FieldValueChanged event fires during a post-back, when the user changes the country value on the page. In both instances, you want to get the current value of the country field and use it to provide a corresponding list of regions. As you recall from the discussion in Chapter 10, the Region template uses the AutoCompleteExtender from the AJAX Control Toolkit to call the RegionCompletionService method that provides the list of regions. The country value serves as the contextKey in the web service call.

As you can see, very few changes were required in the Region field template to support dynamic interaction with the country property. In fact, it is similar to the initial implementation created in Chapter 10, where the name of the country property was hard-coded. With the knowledge of the property name extracted outside of the field template, it is now generic enough to support any region property in the Northwind entity model and could be reused on other projects.

Fixing the FindOtherFieldTemplate Method

Unfortunately, the FindOtherFieldTemplate method provided by Dynamic Data does not work in the dynamically populated FormView and GridView controls and simply returns null. It is possible that this method was created for the initial release of Dynamic Data in service pack 1 of the .NET Framework 3.5 and has not been updated to work with the entity templates introduced in version 4.0. As a workaround, this method was reimplemented in the UnleashedFieldTemplate base class. Listing 11.10 shows a partial listing of the class, with just the code relevant to the current discussion.

Listing 11.10. FindOtherFieldTemplate Method in UnleashedFieldTemplate


using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Web.DynamicData;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Unleashed.DynamicData
{
  public partial class UnleashedFieldTemplate : FieldTemplateUserControl
  {
    protected new UnleashedFieldTemplate FindOtherFieldTemplate(string columnName)
    {
      var template = (UnleashedFieldTemplate)
        base.FindOtherFieldTemplate(columnName);

      if (template == null && !string.IsNullOrEmpty(columnName))
      {
        Control parent = this.FindParentOfAllFieldTemplates();
        template = (UnleashedFieldTemplate)
          FindControlRecursively(parent, "__" + columnName);
      }

      return template;
    }

    private Control FindParentOfAllFieldTemplates()
    {
      Control parent = this.Parent;
      while (parent != null)
      {
        if (parent is GridViewRow ¦¦
            parent is FormViewRow ¦¦
            parent is DetailsViewRow)
          break;
        parent = parent.Parent;
      }

      return parent;
    }

    private static Control FindControlRecursively(Control parent, string id)
    {
      Control control = parent.FindControl(id);
      if (control != null)
        return control;

      foreach (Control child in parent.Controls)
      {
        control = FindControlRecursively(child, id);
        if (control != null &&
            0 == string.Compare(control.ID, id, StringComparison.OrdinalIgnoreCase))
          return control;
      }

      return null;
    }
}


The reimplemented FindOtherFieldTemplate method first tries to call the inherited version, which succeeds if this field template was created in a custom page and the built-in logic was sufficient to find the template. If the template was not found, the FindOtherFieldTemplate first locates the ultimate parent of all field templates of the current entity. It does that by calling the FindParentOfAllFieldTemplates method, which walks up the parent chain until it finds a known container, which could be a GridViewRow, a FormViewRow, or a DetailsViewRow, depending on the type of control used to create the UI—a GridView, a FormView, or a DetailsView, respectively. Having located the ultimate parent, the FindOtherFieldTemplate calls the FindControlRecursively method, which traverses the hierarchy of controls recursively until it finds the target field template.

A couple of odd points in the FindOtherFieldTemplate method are worth special attention. First, the ID of the target field template is determined by concatenating two underscore characters with the column name. This is how DynamicControl generates IDs of field templates in the current version, and the UnleashedFieldTemplate is forced to do it explicitly because this API is not publicly accessible. If the implementation changes in a future version of Dynamic Data, this code will be broken. Hopefully, the original FindOtherFieldTemplate actually works by then and you can simply remove this workaround instead of fixing it.

The other odd point in the FindOtherFieldTemplate method is the additional check performed by the FindControlRecursively method to ensure that the ID of the field template actually matches the ID you are looking for. This is done to ignore the false positive matches from the CheckBoxList and RadioButtonList controls, which override the FindControl method and always return self, regardless of the control ID value that was passed in.

Creating Additional Dynamic Entity Templates

The Default entity template gives your forms a plain single-column layout with labels on the left and data controls on the right. You can control display names and order of properties on this form as well as specify which field templates should be loaded for different properties. This is often adequate for simple entities that have a relatively small number of properties. For larger entities, such as the Employee entity, this simplistic approach to form layout can produce a rather long web page with extra space available on the right side. Normally, when this happens, consider creating a custom entity template and specify a more appropriate layout, perhaps with two columns of labels and data controls. However, it is also possible to create a more sophisticated entity template that will do it dynamically based on the metadata.

DisplayAttribute defines a property called GroupName, which can be used to specify arbitrary strings for grouping controls on a dynamically generated page. For instance, you could use values like “Contact”, “Address”, and “Organization” to generate HTML fieldset controls that display labels and data controls in group boxes similar to Windows applications. Alternatively, you could also use group names such as “Left” and “Right” to assign each of the properties of the Employee entity one of two columns that display side-by-side. Here is a fragment of the Employee metadata class that shows an example of the second approach:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  private abstract class Metadata
  {
    [Display(GroupName = "Left", Order = 100)]
    public object FirstName { get; set; }

    [Display(GroupName = "Left", Order = 110)]
    public object LastName { get; set; }

    [Display(GroupName = "Right", Order = 200)]
    public object Address { get; set; }

    [Display(GroupName = "Right", Order = 210)]
    public object City { get; set; }
  }
}

Combined with the other annotations and several common types of form layouts, the GroupName annotation enables you to continue evolving the entity model without having to create custom entity and page templates even for larger entities, where a single-column layout is no longer practical. However, as you might suspect based on this open-ended description, the GroupName annotation is not supported by the Default entity template that comes with the Dynamic Data project template. This section discusses how to implement additional dynamic entity templates and form layouts.

Extending Metadata to Support Entity UI Hints

Implementing multiple form layouts in a single dynamic entity template is usually not a good idea. On one hand, making a single template responsible for more than one layout would quickly make it too complex to maintain. On the other hand, you do not want to risk breaking all existing dynamic forms by adding a new layout. Instead, it is better to use separate entity templates for different layouts.

Specifying a particular entity template is easy when you are creating a custom page. All you need to do is specify a UIHint attribute value for the DynamicEntity control, which serves as the first candidate name when Dynamic Data searches for the matching entity template. Chapter 4 showed how to take advantage of the UIHint attribute to reuse generic templates for different entity types.

<asp:DynamicEntity runat="server" UIHint="Contact" />

However, there is no built-in capability in Dynamic Data to specify the UI hint in entity metadata. You can solve this problem by defining your own data annotation, such as the EntityUIHintAttribute in the sample solution, and changing the dynamic page templates to initialize the UIHint property of the DynamicEntity control programmatically. Listing 11.11 shows the definition of the new attribute.

Listing 11.11. EntityUIHintAttribute Implementation


using System;

namespace Unleashed.DataAnnotations
{
  [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
  public class EntityUIHintAttribute : Attribute
  {
    public EntityUIHintAttribute(string entityUIHint)
    {
      this.EntityUIHint = entityUIHint;
    }

    public string EntityUIHint { get; private set; }
  }
}


This attribute is applied to an entity class or its associated metadata class. Its constructor takes a string that serves as the entity UI hint and stores it in the EntityUIHint property, which is accessed by the page templates. Here is how definition of the Employee entity class looks with the new hint applied:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  [EntityUIHint("TwoColumn")]
  private abstract class Metadata
  {
    [Display(GroupName = "Left", Order = 100)]
    public object FirstName { get; set; }

    [Display(GroupName = "Left", Order = 110)]
    public object LastName { get; set; }

    [Display(GroupName = "Right", Order = 200)]
    public object Address { get; set; }

    [Display(GroupName = "Right", Order = 210)]
    public object City { get; set; }
  }
}

Similar to the field template hint specified with the UIHintAttribute, the entity hint is a name of a particular template you want Dynamic Data to use instead of the default. In this example, TwoColumn is the name of the new dynamic entity template created shortly to implement a simple two-column form layout.


Note

Because the EntityUIHintAttribute is applied to entity classes, it has to be accessible from the project where your EDMX (entity model) is defined. As you can tell by looking at the namespace declaration, this attribute is defined in the Unleashed.DataAnnotations project of the sample solution accompanying this book. It is referenced by the DataModel project, which contains the entity model itself.


Extending Page Templates to Support EntityUIHintAttribute

To support the entity UI hints, you need to extend the built-in page templates that come with Dynamic Data project templates to initialize the UIHint property of the DynamicEntity control programmatically. Similar to other dynamic controls, this task has to be performed in the Init event handler before the control loads the template. You can modify markup of the three built-in page templates—Details, Insert, and Edit, plus the Delete page template created in Chapter 6, to define the Init event handler:

<asp:DynamicEntity runat="server" OnInit="DynamicEntity_Init" />

Make sure you leave the Mode attribute as is (the Edit and Insert page templates set it to Edit and Insert, respectively). The Init event handler itself is shown next. As you can see, it tries to find the EntityUIHintAttribute in the MetaTable object describing the entity class. If the attribute is found, the code sets the UIHint property of the DynamicEntity control:

protected void DynamicEntity_Init(object sender, EventArgs e)
{
  var entityUIHintAttribute = this.table.Attributes
    .OfType<EntityUIHintAttribute>().FirstOrDefault();
  if (entityUIHintAttribute != null)
    ((DynamicEntity)sender).UIHint = entityUIHintAttribute.EntityUIHint;
}

UnleashedEntity Control

You can reimplement the DynamicEntity initialization logic in all four of the dynamic page templates. With a small number of page templates, this will not be a big effort. However, it is always a good idea to avoid code dulplication. One of the ways to do it is to create an extended version of the DynamicEntity control, such as the UnleashedEntity in the sample solution. Listing 11.12 shows its source code.

Listing 11.12. UnleashedEntity Implementation


using System;
using System.Linq;
using System.Web.DynamicData;
using Unleashed.DataAnnotations;

namespace Unleashed.DynamicData
{
  public class UnleashedEntity: DynamicEntity
  {
    protected override void OnLoad(EventArgs e)
    {
      this.Load += this.InitializeFromMetadata;
      base.OnLoad(e);
    }

    private void InitializeFromMetadata(object sender, EventArgs e)
    {
      if (string.IsNullOrEmpty(this.UIHint))
      {
        MetaTable table = this.FindMetaTable();
        var attribute = table.Attributes.OfType<EntityUIHintAttribute>()
          .FirstOrDefault();
        if (attribute != null)
          this.UIHint = attribute.EntityUIHint;
      }
    }
  }
}


The DynamicEntity control creates the entity template in its OnLoad method; the UnleashedEntity overrides it and relies on the Load event handler called InitializeFromMetadata to initialize the UIHint property. Because the DynamicEntity control does not offer a Table property, the MetaTable object describing the current entity has to be located by calling the FindMetaTable extension method. This method is actually defined in the DynamicDataExtensions class, but calling it with the extension syntax makes the code a lot shorter.


Note

Creating the UnleashedEntity control just for the sake of initializing its UIHint might seem excessive compared to other, simpler ways you could encapsulate this functionality. However, there are other reasons to extend this control as well. Recall the capability implemented in the UnleashedControl to inherit its Mode property from the parent entity template. The UnleashedEntity control implements similar logic, which enables using generic templates discussed in Chapter 4 inside of multimode templates from Chapter 10 without having to implement additional initialization. Refer to the source code accompanying this book to see how this was accomplished.


With all four page templates modified to use the UnleashedEntity instead of the DynamicEntity control, you now have support for metadata-driven entity UI hints in your Dynamic Data web application and can begin implementing additional form layouts as in separate dynamic entity templates.

Building a Dynamic Two-Column Entity Template

As you might recall, the idea of the two-column entity template is that you specify the name of the column where you want a particular property to display using the GroupName property of the DisplayAttribute applied to it in the metadata. In the following code snippet, the FirstName and LastName properties need to be in the column called “Left”, and the Address and City properties need to be in the column called “Right”. The job of the TwoColumn entity template is to define these two columns and instantiate the dynamic controls and their labels in the appropriate column, based on the GroupName annotation:

[MetadataType(typeof(Metadata))]
partial class Employee
{
  [EntityUIHint("TwoColumn")]
  private abstract class Metadata
  {
    [Display(GroupName = "Left", Order = 100)]
    public object FirstName { get; set; }

    [Display(GroupName = "Left", Order = 110)]
    public object LastName { get; set; }

    [Display(GroupName = "Right", Order = 200)]
    public object Address { get; set; }

    [Display(GroupName = "Right", Order = 210)]
    public object City { get; set; }
  }
}

Markup of the TwoColumn entity template is shown in Listing 11.13. By comparing this markup with the Default entity template back in Listing 11.4, you can see that it has an additional HTML table with two cells, each of them containing a nested table and an ASP.NET PlaceHolder control, where the dynamically instantiated controls will be added, depending on their GroupName annotation.

Listing 11.13. TwoColumn Entity Template (Markup)


<%@ Control Language="C#" CodeBehind="TwoColumn.ascx.cs"
  Inherits="WebApplication.DynamicData.EntityTemplates.TwoColumnEntityTemplate" %>
<asp:EntityTemplate runat="server" ID="entityTemplate">
  <ItemTemplate>
    <tr>
      <td class="DDLightHeader">
        <unl:UnleashedLabel runat="server" OnInit="Label_Init" />
      </td>
      <td>
        <unl:UnleashedControl runat="server" OnInit="DynamicControl_Init" />
      </td>
    </tr>
  </ItemTemplate>
</asp:EntityTemplate>
<table>
  <tr>
    <td style="vertical-align:top">
      <table class=" DDDetailsTable">
        <asp:PlaceHolder runat="server" ID="left" />
      </table>
    </td>
    <td style="vertical-align:top">
      <table class=" DDDetailsTable">
        <asp:PlaceHolder runat="server" ID="right" />
      </table>
    </td>
  </tr>
</table>


Listing 11.14 shows the code-behind file of the TwoColumn entity template with the differences from the default entity template shown in bold font. The main difference from the default entity template is located in the OnLoad method of the TwoColumnEntityTemplate. Notice how instead of adding the dynamically created controls to the EntityTemplate itself, it calls the FindGroupPlaceHolder method and adds them to the PlaceHolder control it returns. In other words, the Default entity template uses the EntityTemplate control as both a template to create the dynamic controls and a container to host them. The TwoColumn template, however, uses the EntityTemplate control only as a template, and the two PlaceHolder controls serve as containers for the generated controls.


Note

This is where name of the EntityTemplate control becomes confusing because it collides with the concept of entity templates represented by the EntityTemplateUserControl base class. The EntityTemplate control defines a markup template used to create a DynamicControl and a Label for a single entity property, whereas the EntityTemplateUserControl defines markup for all properties of the entire entity class. Perhaps ColumnTemplate would have been a better name for the EntityTemplate control.


Listing 11.14. TwoColumn Entity Template (Code-Behind)


using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.DynamicData;
using System.Web.UI.WebControls;

namespace WebApplication
{
  public partial class TwoColumnEntityTemplate : EntityTemplateUserControl
  {
    private MetaColumn currentColumn;

    protected override void OnLoad(EventArgs e)
    {
      IEnumerable<MetaColumn> scaffoldColumns =
        this.Table.GetScaffoldColumns(this.Mode, this.ContainerType);
      foreach (MetaColumn column in scaffoldColumns)
      {
        this.currentColumn = column;
        var container = new NamingContainer();
        this.entityTemplate.ItemTemplate.InstantiateIn(container);
        PlaceHolder placeHolder = this.FindGroupPlaceHolder(column);
        placeHolder.Controls.Add(container);
      }
    }

    protected void Label_Init(object sender, EventArgs e)
    {
      ((UnleashedLabel)sender).DataField = this.currentColumn.Name;
    }
    protected void DynamicControl_Init(object sender, EventArgs e)
    {
      ((UnleashedControl)sender).DataField = this.currentColumn.Name;
    }

    private PlaceHolder FindGroupPlaceHolder(MetaColumn column)
    {
      PlaceHolder placeHolder = null;
      var display = column.Attributes.OfType<DisplayAttribute>().FirstOrDefault();
      if (display != null && !string.IsNullOrWhiteSpace(display.GroupName))
        placeHolder = (PlaceHolder)this.FindControl(display.GroupName);
      return placeHolder ?? this.left;
    }
  }
}


The FindGroupPlaceHolder method tries to find a DisplayAttribute applied to the entity property described by the MetaColumn object it receives. If the DisplayAttribute is found and has the GroupName property specified, it calls the FindControl method inherited from the ASP.NET’s Control base class to find the PlaceHolder with the matching name. If the entity property does not have a GroupName annotation or if the PlaceHolder with the specified name could not be found, the “leftPlaceHolder is returned by default. In other words, unless you specify otherwise, the TwoColumn entity template displays controls on the left side of the page.

Figure 11.8 shows how the Employee details page can look when the EntityUIHintAttribute is applied to the entity class and the GroupName annotation is specified for the appropriate entity properties.

Image

Figure 11.8. TwoColumn entity template.


Note

Although it is hard to tell by just looking at the code in the OnLoad method of the TwoColumn entity template, the Order annotation is still as relevant here as it is in the Default (single-column) template. Because the GetScaffoldColumn method returns MetaColumn objects sorted by the Order value, the controls are instantiated and added to the two PlaceHolder controls in the proper order, as specified in the metadata. Keep in mind that both entity templates (single-item forms) and list page templates use the Order annotation to determine order of controls and columns respectively. Dealing with a large number of properties can be still difficult when it comes to the GridView columns.


The approach used to create the TwoColumn entity template can also work for advanced page layouts that have more “zones” than just two, such as the popular “two columns with a footer” layout used by many applications including the Work item Form in Visual Studio Team Explorer. It typically has two columns with small data controls that occupy the top half of the page and a combined footer area contains big data controls, such as multiline text boxes, that need to be much wider than a single column would allow.

To support a more complex form layout, you would simply need to create a new entity template with an appropriate structure of HTML table (or div, if you prefer using CSS) elements and place a PlaceHolder control in locations where you want the data controls to appear and give them appropriate names, such as “Left”, “Right”, “Bottom”, and so on. As long as you use the matching names in the GroupName annotations, the FindGroupPlaceHolder method will be able to find them, no matter how complex the form is.

Summary

The ability to build user interfaces based on metadata is one of the key advantages Dynamic Data offers. It dramatically reduces the amount of presentation code you have to write and maintain over the lifetime of an application and, when used judiciously, significantly increases the speed of development. By postponing implementation of the user interface, Dynamic Data allows you to focus on the most important part of the application—its entity model. You can then refine it through short successive iterations, collecting feedback from business users based on actual working applications instead of diagrams, and greatly reduce risk and uncertainty in the project.

Out of the box, Dynamic Data offers robust capabilities for building single-column forms through metadata. The DisplayAttribute allows you to control display names and order of entity properties on forms and in grids. The DisplayFormatAttribute offers configuration options to control appearance of values in both Read-only and Edit mode. The DataTypeAttribute and the UIHintAttribute can be used to override the default rules Dynamic Data uses when selecting a field template for a particular property. The EnumDataTypeAttribute can be applied to an integral property to associate it with a list of predefined values that are presented to the users as a DropDownList control by the Enumeration field template.

This chapter discussed in detail how to overcome some of the limitations of Dynamic Data. The Enumeration field templates can be extended to take advantage of the DisplayAttribute to specify human-readable display name for enum items. Dynamic Data controls can be also extended to support field template parameters specified declaratively with the UIHintAttribute. Although it should be done with great caution, interaction between field templates can be configured through the metadata. Finally, additional dynamic entity templates can be implemented to take advantage of extended data annotations, such as the EntityUIHintAttribute discussed in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.30.178