The CategoryAttribute and Dynamic Data

The System.ComponentModel.CategoryAttribute has long been around. It’s generally used for annotating properties on web controls so that design mode’s Property Editor can organize your properties.

I think the CategoryAttribute makes a lot of sense as business logic in your Entity classes. By categorizing properties, your user interface can get creative:

  • Automatic scaffolding can be generated based on matched categories. This requires writing a custom FieldGenerator class.
  • Field Templates can customize themselves. For example, the Text.ascx Field Template uses a default style sheet class on its <asp:Label> control. When the Category is set to “Title”, the Field Template can change the style sheet class to one that uses a different font.

The CategoryAttribute takes one value, a string. This is supposed to represent a single category.

[Category("Title")]
public object Name { get; set; }

I recommend allowing multiple categories in a pipe character delimited list:

[Category("Title|ProperName")]
public object Name { get; set; }

This allows much more flexibility.

The CategoryManager class

Add this class to your application. It checks your requested category names against those in the CategoryAttribute.

using System.Web.DynamicData;
using System.ComponentModel;
using System.Text.RegularExpressions;
using System.Linq;
public static class CategoryManager
{
/// <summary>
/// Checks the MetaColumn's CategoryAttribute against the filtering rules to determine if there is a match.
/// </summary>
/// <remarks>
/// <para>Works as a Extender method on the MetaColumn class.</para>
/// </remarks>
/// <param name="pColumn">The MetaColumn that has a list of attributes. They are searched for the CategoryAttribute.</param>
/// <param name="pCategories">A pipe delimited list of Categories that must either be present or absent based
/// on pExcludeCategories.</param>
/// <param name="pExcludeCategories">When true, none of the category names in pCategories must be in the CategoryAttribute.</param>
/// <param name="pCategoriesWithNoMatchAreExcluded"> Determines what happens when
/// pCategories does not contain a matching name to the CategoryAttribute’s value,
/// including when the MetaColumn lacks a CategoryAttribute.</param>
/// <returns>When true, keep the column</returns>
public static bool CheckCategories(this MetaColumn pColumn, string pCategories, bool pExcludeCategories, bool pCategoriesWithNoMatchAreExcluded)
{
CategoryAttribute vCA = pColumn.Attributes.OfType<CategoryAttribute>().FirstOrDefault();
return CheckCategories(vCA, pCategories, pExcludeCategories, pCategoriesWithNoMatchAreExcluded);
} // CheckCategories
/// <summary>
/// Checks the CategoryAttribute against the filtering rules to determine if there is a match.
/// </summary>
/// <param name="pCategoryAttribute">The CategoryAttribute. Can be null.</param>
/// <param name="pCategories">A pipe delimited list of Categories that must either be present or absent based
/// on pExcludeCategories.</param>
/// <param name="pExcludeCategories">When true, none of the category names in pCategories must be in the CategoryAttribute.</param>
/// <param name="pCategoriesWithNoMatchAreExcluded"> Determines what happens when
/// pCategories does not contain a matching name to the CategoryAttribute’s value,
/// including when the CategoryAttribute is null.</param>
/// <returns>When true, the rules matched the CategoryAttribute.</returns>
public static bool CheckCategories(CategoryAttribute pCategoryAttribute, string pCategories, bool pExcludeCategories, bool pCategoriesWithNoMatchAreExcluded)
{
if (pCategoryAttribute == null)
return !pCategoriesWithNoMatchAreExcluded;
// Both CategoryAttribute.Value and pCategories are pipe delimited lists
// Convert CategoryAttribute.Value to a regex that can find one match in Categories.
string vPattern = cPipeDelimitedLeft + pCategories + cPipeDelimitedRight;
if (Regex.IsMatch(pCategoryAttribute.Category, vPattern, RegexOptions.IgnoreCase))
return !pExcludeCategories;
return pCategoriesWithNoMatchAreExcluded;
}
/// <summary>
/// When searching a pipe delimited list for a match, this regex should appear
/// first, before the string containing the list.
/// </summary>
const string cPipeDelimitedLeft = @"(^|[\|])(";
/// <summary>
/// When searching a pipe delimited list for a match, this regex should appear
/// last, after the string containing the list.
/// </summary>
const string cPipeDelimitedRight = @")($|[\|])";
}



Automatic Scaffolding using Categories

Here is a basic implementation of IAutoFieldGenerator with support for category attribute searches.

 

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

public class CategoryFieldsGenerator : c
{
private MetaTable _metaTable;

/// <summary>
/// The mode used by the caller: ReadOnly, Edit, Insert.
/// </summary>
public DataBoundControlMode Mode { get; set; }
/// <summary>
/// Is the interface a list or item?
/// </summary>
public ContainerType ContainerType { get; set; }

/// <summary>
/// When true, ignore the MetaColumn.IsScaffold property. Just use the CategoryAttribute
/// even if [ScaffoldColumnAttribute(false)].
/// When false, limit to those that are MetaColumn.IsScaffold = true
/// </summary>
public bool IgnoreScaffold { get; set; }


/// <summary>
/// A pipe delimited list of the categories either to include or exclude in the list of
/// DynamicFields returned.
/// </summary>
/// <value>
/// <para>Use a Pipe delimited list for multiple items; matching is case insensitive.</para>
/// </value>
public string Categories { get; set; }

/// <summary>
/// Determines what happens when the Categories property contains a matching name
/// to the CategoryAttribute’s value. Normally when the Category name is found
/// in the CategoryAttribute, that NamedStyle is a match. This property can make
/// that NamedStyle not match.
/// </summary>
/// <value>
/// <para>When true, exclude data fields with any of these category names.</para>
/// <para>When false, include data fields with any of these category names.</para>
/// </value>
public bool ExcludeCategories { get; set; }

/// <summary>
/// Determines what happens when the Categories property does not contain a matching name to the CategoryAttribute’s value,
/// including when the data field lacks a CategoryAttribute.
/// </summary>
/// <value>
/// <para>When true, the DynamicField is omitted.</para>
/// <para>When false, the DynamicField is included.</para>
/// </value>
public bool CategoriesWithNoMatchAreExcluded { get; set; }

public CategoryFieldsGenerator(MetaTable table, DataBoundControlMode mode, ContainerType containerType,
string categories, bool excludeCategories, bool categoriesWithNoMatchAreExcluded)
{
_metaTable = table;
Mode = mode;
ContainerType = containerType;
Categories = categories;
ExcludeCategories = excludeCategories;
CategoriesWithNoMatchAreExcluded = categoriesWithNoMatchAreExcluded;
}
public CategoryFieldsGenerator(MetaTable table, DataBoundControlMode mode, ContainerType containerType,
string categories) : this (table, mode, containerType, categories, false, true)
{
}

public ICollection GenerateFields(Control control)
{

List<DynamicField> fields = new List<DynamicField>();
IEnumerable<MetaColumn> columns = IgnoreScaffold ? _metaTable.Columns : _metaTable.GetScaffoldColumns(Mode, ContainerType);
foreach (MetaColumn column in columns)
{
if (CategoryManager.CheckCategories(column, Categories, ExcludeCategories, CategoriesWithNoMatchAreExcluded))
fields.Add(CreateField(column));
}

return fields;
}

protected virtual DynamicField CreateField(MetaColumn column)
{
DynamicField dynamicField = new DynamicField();
dynamicField.DataField = column.Name;
dynamicField.HeaderText = ContainerType == ContainerType.List ? column.ShortDisplayName : column.DisplayName;
return dynamicField;
}
}


In your web form’s Page_Init() method, create the CategoryFieldGenerator and assign it to the GridView.ColumnsGenerator or DetailsView.RowsGenerator. This example includes all columns that have the category “Small”, so long as they are also scaffoldable. (IgnoreScaffold property is false.)

table = LinqDataSource1.GetTable();
GridView1.ColumnsGenerator = new CategoryFieldGenerator(table, DataBoundControlMode.ReadOnly, ContainerType.List, "Small");


Changing the Style Sheets within Field Templates

In Page_Load, update the CssClass property on the desired controls based on the category. This example looks for “Title” and switches to the alternate style sheet.

<script runat="server">
   1:  

   2: public override Control DataControl { get { return FieldValueLabel; }}

   3: protected void Page_Load(object sender, EventArgs e)

   4: {

   5:     if (CategoryManager.CheckCategories("Title", false, false))

   6:         FieldValueLabel.CssClass = "TitleLabel";

   7:     else

   8:         FieldValueLabel.CssClass = "StndLabel";

   9: }
</script>
<asp:Label ID="FieldValueLabel" runat="server" ></asp:Label>

Peter’s Soapbox

I encourage the ASP.NET Dynamic Data team to include support for the CategoryAttribute (or some variation of it) in a future release. I have no problem with MS or anyone else using the code of this posting. I consider it public domain and does not require a license.

No Comments