Descriptor objects : Exposing the structure and business logic to consumers

This series looks at the Business Logic Layer of an application from how my Peter’s Business Logic Driven UI (“BLD”) implements it. Use the “Understanding the BLL” Category to see all articles.

So far, we’ve learned about the Entity classes and how they maintain your business rules. Business rules are only useful if your code can retrieve them. Since most business rules are DataAnnotation attributes assigned to properties as metadata, its pretty easy to envision how to retrieve them: call the PropertyInfo.GetCustomAttributes(type) method.

[C#]

using System.Reflection;
PropertyInfo vPropertyInfo = typeof(Category).GetProperty("CategoryName");
object[] vAttributes = vPropertyInfo.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute));
if (vAttributes.Length > 0)
{
    RequiredAttribute vRequiredAttribute = (RequiredAttribute)vAttributes[0];
// use vRequiredAttribute
}

[VB]

Imports System.Reflection
Dim vPropertyInfo As PropertyInfo = GetType(Category).GetProperty("CategoryName")
Dim vAttributes As Object() = vPropertyInfo.GetCustomAttributes(GetType(System.ComponentModel.DataAnnotations.RequiredAttribute))
If vAttributes.Length > 0 Then
 
   Dim vRequiredAttribute As RequiredAttribute = CType(vAttributes(0), RequiredAttribute)
' use vRequiredAttribute
End If

There are an enormous number of details about a single property in an Entity class. They include what the DataAnnotation attributes supply and structural information such as the property name, the associated column name, and its type. BLD uses Descriptor classes to capture all of these details once and make them easily accessible to you. There are four types of Descriptor classes:

  • DataFieldDescriptor – Describes a DataField (property on the Entity class) gathering information from both DAL and BLL Entity classes. The DAL supplies structural information, like the property name, column name, and type. The BLL supplies its list of DataAnnotation attributes.
  • RelationshipDataFieldDescriptor – Some DataFields describe relationships and are not actually columns in the table. These DataFields are described with this specialized DataFieldDescriptor class that identifies the destination Entity class of the relationship and how the relationship is structured.
  • EntityDescriptor – Describes an Entity class. It keeps a list of all DataFieldDescriptors for the Entity class. It knows information about the actual table and it has a list of DataAnnotation attributes defined on the Entity class itself.
  • DataContextDescriptor – Describes the DataContext class (a.k.a. “domain model” and “ObjectContext”). It keeps a list of all EntityDescriptors. It hosts the RestrictionManager, a class that defines how security on tables and columns is implemented by user role. It also has event handlers, including one to override how DataAnnotation attributes are identified so you can get them from a database or XML file instead of the Entity Metadata class.

I think the best way to see what Descriptors can offer your code is to see them in action. Please visit: http://learningbld.peterblum.com/DescriptorsBrowser.aspx. This page uses the Descriptors defined by BLD’s Learning site. Start with the two-table database I provide for BLD’s Walkthrough:

  • Select PeterBlum.Walkthrough.Appointment.AppointmentDemoDataContext from the Registered DataContext dropdownlist.
  • Click AppointmentDemo from the EntityTypes column.
  • Start playing around, clicking on items in the DataFields and List of attributes columns.

Descriptors are defined in the Business Logic Layer and are used by the UI as it applies the business rules. They are available to your CRUD code, which is located in the BLL. For example, the UI may pass a list of DataFields and values that it wants to update. Your Update method (part of the CRUD) creates its code based on that list, using the DataFieldDescriptor to know the Column name and database type of each value.

Here is an example of how to retrieve the DataFieldDescriptor for the CategoryName DataField on the Category Entity class. Let’s suppose it’s used in the user interface to disable a TextBox control when the field is read-only and assigns text to the Label associated with the textbox from the DisplayNameAttribute (a DataAnnotation attribute that provides a user friendly name for the column).

[C#]

using PeterBlum.DES.DataAnnotations.Descriptors;
BaseDataFieldDescriptor vDataFieldDescriptor = DescriptorManager.GetDataFieldDescriptor(typeof(Category), "CategoryName");
if (vDataFieldDescriptor.IsReadOnly)
   TextBox1.Enabled = false;
Label1.Text = vDataFieldDescriptor.GetLocalizedDisplayName(System.Globalization.CultureInfo.CurrentUICulture);

[VB]

Imports PeterBlum.DES.DataAnnotations.Descriptors
Dim vDataFieldDescriptor As BaseDataFieldDescriptor = DescriptorManager.GetDataFieldDescriptor(GetType(Category), "CategoryName")
If vDataFieldDescriptor.IsReadOnly Then
   TextBox1.Enabled = False
End If
Label1.Text = vDataFieldDescriptor.GetLocalizedDisplayName(System.Globalization.CultureInfo.CurrentUICulture)

Digging a little deeper

Normally you would get and set a property in a class using the System.Reflection.PropertyInfo object’s GetValue() and SetValue() method. The DataFieldDescriptor offers its own GetValue() and SetValue() methods, which are wrappers around the PropertyInfo methods by default. Your UI and CRUD methods should always use the DataFieldDescriptor’s methods because it allows DataFieldDescriptors to handle special cases.

Calculated fields are good examples. Suppose you want a calculated DataField that creates a full address string from columns that make up the parts (Address, City, Country, etc). BLD lets you create a custom DataFieldDescriptor. By overriding the GetValue() method, your code can create this string.

The DataFieldDescriptor also knows how to validate with its Validate() method. This method goes through all validation-oriented DataAnnotation attributes and populates a list of errors. The EntityDescriptor also has a Validate() method. It goes through all DataFieldDescriptors, calling their Validate() method to create a complete list of errors. Use the Validate() method in your Update and Insert methods in the BLL.

Here are some additional methods of interest on DataFieldDescriptor:

  • ChangeType() – Pass it a value. If its not identical to the property’s type, it will attempt to convert it to the correct type. It even handles string to native type conversion.
  • ConvertNativeToFormattedString() – Converts a value into a string, applying localization and business rules such as those from the System.ComponentModel.DataAnnotations.DisplayFormatAttribute.
  • CanTakeActionOnColumn() – Determines if the current user is allowed to take the action passed in on the column. For example, if the user’s role is “Sales”, can they edit the “UnitPrice” column on the Product table? There is a similar method on the EntityDescriptor for actions on Tables.
  • GetLocalizedDisplayName(), GetLocalizedShortDisplayName(), GetLocalizedDescription(), GetLocalizedPrompt() – Returns the localized string associated with the business rules for DisplayName, ShortDisplayName, Description, and Prompt.

For more on Descriptors, start with the "Descriptor classes: The structure and business rules of your database" topic in the Learning BLD site. To go deeper, see the topics in the BLD User’s Guide.

No Comments