Sunday 28 February 2010

SiteMapProvider for Dynamic Data (.Net 3.5 SP1 and .Net 4.0) (UPDATED)

There was a post on the DD Forum a little while ago (Last week when I wrote this article) and it’s the issue of menus and site navigation in Dynamic Data, I’ve had several go’s at this on several projects and although I have had workable solutions I felt I was reinventing the wheel each time. So after the post politely asking for me to go into it a little deeper I made some suggestions on the forum that afterwards I felt were still going about this in the wrong way. What I felt we needed was our own SiteMapDataSourceBlushingand of course I was wrong, I discovered after a little research was I needed a SiteMapProvider which would plug into the SiteMapDataSource via the web.config allowing me to provide my own solution to navigation and menus. This is very similar the MembersipProvider and RolesProvider which makes this really coolBig Grin

So now we will have a solution that is easy to deploy and maintain, so in good Blue Peter style here are the things we are going to need:


    The Menu attribute
    The SiteMapProvider
    Updates to the Web.Config
    Metadata
    And a Menu and SiteMapPath (bread crumb) controls
    web.sitemap file for custom paths (non Model pages)

The MenuAttribute

At first I hade some code like Listing 1 here I generated the code in the page (site.master) and bound it to an XmlDataSource this worked after a fashion and produced the menus I wanted but they were organised by the DB structure not really what I wanted.

protected void Page_Load(object sender, EventArgs e)
{
    //System.Collections.IList visibleTables = MetaModel.Default.VisibleTables;
    System.Collections.IList visibleTables = MetaModel.Default.VisibleTables;
    if (visibleTables.Count == 0)
    {
        throw new InvalidOperationException("There are no accessible tables. Make..");
    }

    var root = new XElement("home");

    foreach (var table in MetaModel.Default.VisibleTables)
    {
        root.Add(GetChildren(table));
    }
    XmlDataSource1.Data = root.ToString();
    Menu1.DataSource = XmlDataSource1;
    Menu1.Orientation = Orientation.Horizontal;
    Menu1.DataBind();
}

private XElement GetChildren(MetaTable parent)
{
    XElement children =
        new XElement("Menu",
            new XAttribute("title", parent.DisplayName),
            new XAttribute("url", parent.ListActionPath),
                    from c in parent.Columns
                    where c.GetType() == typeof(MetaChildrenColumn) &&
                        ((MetaChildrenColumn)c).ChildTable.Name != parent.Name
                    orderby c.DisplayName
                    select GetChildren(((MetaChildrenColumn)c).ChildTable));

    return children;
}

Listing 1 – old Menu generating code

I suppose I could have used a sitemap file (“Web.sitemap” by default) I decided after a few stunted attempts with client sites that although this gave you the control it was tedious and boring especially with an constantly changing site.

So my final design was to have an attribute to apply to the metadata that would allow me to impose the site structure I wanted in the same place I was applying the all my other attributes (keep it all in one place is what I say then when you go looking it’s easy to find.)

/// <summary>
/// Attribute to identify which column to use as a 
/// parent column for the child column to depend upon
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class MenuAttribute : Attribute, IComparable
{
    public static MenuAttribute Default = new MenuAttribute();

    /// <summary>
    /// Gets or sets the name of the menu.
    /// </summary>
    /// <value>The name of the menu.</value>
    public String Name { get; private set; }

    /// <summary>
    /// Gets or sets the parent.
    /// </summary>
    /// <value>The parent.</value>
    public String Parent { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether this <see cref="MenuAttribute"/> is show.
    /// </summary>
    /// <value><c>true</c> if show; otherwise, <c>false</c>.</value>
    public Boolean Show { get; set; }

    /// <summary>
    /// Gets or sets the order.
    /// </summary>
    /// <value>The order.</value>
    public int Order { get; set; }

    /// <summary>
    /// Gets or sets the image path.
    /// </summary>
    /// <value>The image path.</value>
    public String ImagePath { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="MenuAttribute"/> class.
    /// </summary>
    public MenuAttribute()
    {
        Name = String.Empty;
        Parent = String.Empty;
        ImagePath = String.Empty;
        Show = false;
        Order = 0;
    }
    /// <summary>
    /// Initializes a new instance of the <see cref="MenuAttribute"/> class.
    /// </summary>
    /// <param name="menuName">Name of the menu.</param>
    public MenuAttribute(String menuName)
    {
        Name = menuName;
    }

    #region IComparable Members
    public int CompareTo(object obj)
    {
        return Order - ((MenuAttribute)obj).Order;
    }
    #endregion
}

Listing 2 – the Menu attribute

I decided I wanted to be able to specify which table are in the root of the menu which are children and of which table and also if a table is shown or not, see Listing 2. to make a table appear in the root of the menu structure you need to Parent to be empty and Show must be true*  and Children must Have the Parent set to the menu Name if specified or Table Name and be set to show.

* This is because I will be using my GetAttributeOrDefault extension method see Listing 3
/// <summary>
/// Get the attribute or a default instance of the attribute
/// if the Table attribute do not contain the attribute
/// </summary>
/// <typeparam name="T">Attribute type</typeparam>
/// <param name="table">
/// Table to search for the attribute on.
/// </param>
/// <returns>
/// The found attribute or a default 
/// instance of the attribute of type T
/// </returns>
public static T GetAttributeOrDefault<T>(this MetaTable table) where T : Attribute, new()
{
    return table.Attributes.OfType<T>().DefaultIfEmpty(new T()).FirstOrDefault();
}

Listing 3 GetAttributeOrDefault extension method.

I also threw in an Order property so the menus could be sorted on both the root and sub menus and an ImagePath property so that you can specify an image. Name is optional and in not specified it will user the DisplayName of the table.

The SiteMapProvider

This will go away and read the MetaModel and produce the site map from that instead of a web.sitemap file.

#region Constants
private const String PROVIDER_NAME = "MetaDataSiteMapProvider";
private const string CACHE_DEPENDENCY_NAME = "MetaDataSiteMapCacheDependency";
private const String DESCRIPTION = "description";
private const String SITE_MAP_FILE = "SiteMapFile";
private const String DEFAULT_SITE_MAP_FILE = "DynamicData.sitemap";
#endregion

#region Fields
private readonly object objLock = new object();
private SiteMapNode rootNode;
private String _siteMapFile;
#endregion

#region Properties
private MetaModel _model;
public MetaModel Model
{
    get
    {
        if (_model == null)
            _model = MetaModel.Default;
        return this._model;
    }
    set { this._model = value; }
}
#endregion

#region Initializartion
public override void Initialize(string name, NameValueCollection config)
{
    if (config == null)
        throw new ArgumentNullException("config");

    // get provider name
    if (String.IsNullOrEmpty(name))
        name = "MetaDataSiteMapProvider";

    // get custom mappings
    if (!string.IsNullOrEmpty(config[SITE_MAP_FILE]))
        _siteMapFile = config[SITE_MAP_FILE];
    else
        _siteMapFile = DEFAULT_SITE_MAP_FILE;

    // get description
    if (string.IsNullOrEmpty(config[DESCRIPTION]))
    {
        config.Remove(DESCRIPTION);
        config.Add(DESCRIPTION, "MetaData Site Map Provider for Dynamic Data");
    }

    base.Initialize(name, config);
}
#endregion

/// <summary>
/// Loads the site map information from 
/// persistent storage and builds it in memory.
/// </summary>
/// <returns>
/// The root <see cref="T:System.Web.SiteMapNode"/> 
/// of the site map navigation structure.
/// </returns>
public override SiteMapNode BuildSiteMap()
{
    // need to make sure we start with a clean slate
    if (rootNode != null)
        return rootNode;

    lock (objLock)
    {
        rootNode = new SiteMapNode(this, "root");

        // get tables for
        var tables = from t in Model.Tables
                     where String.IsNullOrEmpty(t.GetAttributeOrDefault<MenuAttribute>().Parent) &&
                        t.GetAttributeOrDefault<MenuAttribute>().Show
                     // sort first by menu order then by name.
                     orderby t.GetAttributeOrDefault<MenuAttribute>(), t.DisplayName
                     select t;

        // get external content.
        var sitemapFile = HttpContext.Current.Server.MapPath("~/" + _siteMapFile);
        if (File.Exists(sitemapFile))
        {
            var sitemap = XDocument.Load(sitemapFile);
            var elements = sitemap.Descendants().Descendants().Descendants();
            foreach (var element in elements)
            {
                var provider = element.Attributes().FirstOrDefault(a => a.Name == "provider");
                if (provider != null &&
                    !String.IsNullOrEmpty(provider.Value) &&
                    provider.Value == PROVIDER_NAME)
                {
                    foreach (var table in tables)
                    {
                        SetChildren(table, rootNode);
                    }
                }
                else
                {
                    SetXmlChildren(element, rootNode);
                }
            }
        }
        else
        {
            foreach (var table in tables)
            {
                SetChildren(table, rootNode);
            }
        }
    }

    // not sure if this is needed no real explination
    // was given in the samples I've seen.
    HttpRuntime.Cache.Insert(CACHE_DEPENDENCY_NAME, new object());

    return rootNode;
}

private void SetXmlChildren(XElement element, SiteMapNode parentNode)
{
    var url = element.Attributes().First(a => a.Name == "url");
    var node = new SiteMapNode(this, url.Value, url.Value);

    foreach (var attribute in element.Attributes())
    {
        switch (attribute.Name.ToString())
        {
            case "description":
                node.Description = attribute.Value;
                break;
            case "resourceKey":
                node.ResourceKey = attribute.Value;
                break;
            case "roles":
                node.Roles = attribute.Value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                break;
            case "title":
                node.Title = attribute.Value;
                break;
            case "siteMapFile":
            case "securityTrimmingEnabled":
            case "provider":
            case "url":
            default:
                break;
        }
    }

    AddNode(node, parentNode);

    if (element.HasElements)
    {
        foreach (var childElement in element.Descendants())
        {
            SetXmlChildren(childElement, node);
        }
    }
}

/// <summary>
/// Sets the children nodes of the current node.
/// </summary>
/// <param name="parentTable">The parent table.</param>
/// <param name="parentNode">The parent node.</param>
private void SetChildren(MetaTable parentTable, SiteMapNode parentNode)
{
    String imageUrl = String.Empty;
    var description = parentTable.GetAttribute<DescriptionAttribute>();
    var menuAttribute = parentTable.GetAttribute<MenuAttribute>();
    if (menuAttribute != null)
        imageUrl = menuAttribute.ImagePath;

    // get extra attributes, I'm just going to 
    // use the ImageUrl for use in menus etc.
    NameValueCollection attributes = null;
    if (String.IsNullOrEmpty(imageUrl))
    {
        attributes = new NameValueCollection();
        attributes.Add("ImageUrl", imageUrl);
    }

    // get the title
    var menuTitle = !String.IsNullOrEmpty(menuAttribute.Name)
        ? menuAttribute.Name : parentTable.DisplayName;

    // get the description if attribute has 
    // been applied or DisplayName if not.
    var menuDescription = description != null
        ? description.Description : parentTable.DisplayName;

    // note resource keys are not used I'm 
    // not bothering with localization.
    var node = new SiteMapNode(
        this,
        // note Key and Url must match
        // for slected menus to showup
        parentTable.ListActionPath,
        parentTable.ListActionPath,
        menuTitle,
        menuDescription,
        null,
        attributes,
        null,
        "");

    // we can't add two nodes with same URL an 
    // InvalidOperationException with be thrown if we do
    AddNode(node, parentNode);

    // get the children nodes of this node.
    var tables = from t in Model.Tables
                 where !String.IsNullOrEmpty(t.GetAttributeOrDefault<MenuAttribute>().Parent) &&
                   t.GetAttributeOrDefault<MenuAttribute>().Parent == menuTitle &&
                   t.GetAttributeOrDefault<MenuAttribute>().Show
                 orderby t.GetAttributeOrDefault<MenuAttribute>(), t.DisplayName
                 select t;

    // add children of current node
    foreach (var t in tables)
    {
        // call this method recursively.
        SetChildren(t, node);
    }
}

/// <summary>
/// Retrieves the root node of all the nodes that 
/// are currently managed by the current provider.
/// </summary>
/// <returns>
/// A <see cref="T:System.Web.SiteMapNode"/> that 
/// represents the root node of the set of nodes 
/// that the current provider manages.
/// </returns>
protected override SiteMapNode GetRootNodeCore()
{
    BuildSiteMap();
    return rootNode;
}

/// <summary>
/// Removes all elements in the collections of 
/// child and parent site map nodes that the 
/// <see cref="T:System.Web.StaticSiteMapProvider"/> 
/// tracks as part of its state.
/// </summary>
protected override void Clear()
{
    lock (objLock)
    {
        this.rootNode = null;
        base.Clear();
    }
}

Listing – 4 the SiteMapProvider

Updated: I have made a few changes in Listing 4 I found that I was still missing something mainly an easy way to add custom page mappings (SiteMapNodes) so I have added the facility to use a web.sitemap my default file name is DyanmicData.sitemap.

With this added and an entry for the provider set to "MetaDataSiteMapProvider" the standard nodes are added and when the node (and “there can be only one”) the Metadata nodes are added. See Figure 1.

In the SiteMapProvider Listing 4 the two main methods are BuildSiteMap and SetChildren; BuildSiteMap  starts by building the root menu and then calls SetChildren for each menu entry in the root to create the sub menus entries (and their sub menus as it calls it’s self recursively).

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="" title=""  description="">
      <siteMapNode url="~/Default.aspx" title="Home"  description="Home page" roles="*" />
      <siteMapNode provider="MetaDataSiteMapProvider"  />
      <siteMapNode url="~/Admin.aspx" title="Admin"  description="Site Administration" roles="*" />
    </siteMapNode>
</siteMap>

Listing 5 – the DynamicData.sitemap file

To make this work we need an entry in the web.config file see Listing 5 this entry live inside the <system.web> tag.

<siteMap defaultProvider="MetaDataSiteMapProvider">
	<providers>
		<add name="MetaDataSiteMapProvider" 
		    Description="MetaData Site Map Provider for Dynamic Data" 
		    type="NotAClue.Web.DynamicData.MetaDataSiteMapProvider, NotAClue.Web.DynamicData"/>
	</providers>
</siteMap>

Listing 6 – SiteMap entry in web.config.

Adding Menu and SiteMapPath controls to Site.Master

Now we need to add a Menu or TreeView control to the site.master page (I am also going to add a SiteMapPath control as a bread crumb)

<div>
    <asp:Menu ID="Menu1" runat="server" BackColor="#E3EAEB" 
        DataSourceID="SiteMapDataSource1" DynamicHorizontalOffset="2" 
        Font-Names="Verdana" Font-Size="8pt" ForeColor="#666666" 
        Orientation="Horizontal" StaticSubMenuIndent="10px" 
        StaticDisplayLevels="2">
        <StaticSelectedStyle BackColor="#1C5E55" />
        <StaticMenuItemStyle HorizontalPadding="5px" VerticalPadding="2px" />
        <DynamicHoverStyle BackColor="#666666" ForeColor="White" />
        <DynamicMenuStyle BackColor="#E3EAEB" />
        <DynamicSelectedStyle BackColor="#1C5E55" />
        <DynamicMenuItemStyle HorizontalPadding="5px" VerticalPadding="2px" />
        <StaticHoverStyle BackColor="#666666" ForeColor="White" />
    </asp:Menu>
    <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" ShowStartingNode="False" />
</div>
<div>
    <asp:SiteMapPath ID="SiteMapPath1" runat="server" Font-Names="Verdana" 
        Font-Size="8pt" PathSeparator=" > ">
        <PathSeparatorStyle Font-Bold="True" ForeColor="#1C5E55" />
        <CurrentNodeStyle ForeColor="#333333" />
        <NodeStyle Font-Bold="True" ForeColor="#666666" />
        <RootNodeStyle Font-Bold="True" ForeColor="#1C5E55" />
    </asp:SiteMapPath>
</div>

Listing 7 – adding Menu and SiteMapPath controls to site.master.

In Listing 6 I’ve just dragged and dropped ASP.Net Menu and SiteMapPath web controls to the page an configured the Menu by adding a new SiteMapDataSource control.

Note: You could also set the SiteMapProvider to our MetaDataSiteMapProvider this is not really required as we have set as default in the web.config.

Then I have used the Auto Format… option to give both the Menu and SiteMapPath some styling.

Note: In ASP.Net 4.0 the Menu Control like many other controls now boasts cleaner mark-up; in the Menus case it now uses un-ordered lists to produce the menu.

Sample Metadata using Northwind

[Menu(Show = true, Parent = "Products", Order = 0)]
partial class Category { }

[Menu(Show = true, Parent = "Employees", Order = 0)]
partial class Territory { }

partial class CustomerCustomerDemo { }

partial class CustomerDemographic { }

[Menu("Customers", Show = true, Order = 0)]
partial class Customer { }

[Menu("Employees", Show = true, Order = 2)]
partial class Employee { }

partial class EmployeeTerritory { }

[Menu("Order Details", Show = true, Parent = "Orders", Order = 0)]
partial class Order_Detail { }

[Menu("Orders", Show = true, Order = 1)]
partial class Order { }

[Menu("Products", Show = true, Order = 3)]
partial class Product { }

[Menu("Regions", Show = true, Parent = "Territories", Order = 0)]
partial class Region { }

[Menu("Shippers", Show = true, Parent = "Orders", Order = 0)]
partial class Shipper { }

[Menu("Suppliers", Show = true, Parent = "Products", Order = 1)]
partial class Supplier { }

Listing 8 – sample metadata.

Note: I’ve trimmed the metadata right down removing the metadata classes to keep it brief.

The Result

Dynamic Data Menus and bread crumb (SiteMapPath) Asp.Net 4.0 Tabbed Menu

Figure 1 – Dynamic Data Menus and bread crumb (SiteMapPath)

Downloads

Left is the ASP.Net 3.5 SP1 and right is the .Net 4.0 version & now with the new Tabbed Menu from the new ASP.Net project template.

Wednesday 17 February 2010

DisplayAttribute “Prompt” or Watermark in Dynamic Data 4 (Visual Studio 2010 & .Net 4.0)

In Figure 1 there is another property called “Prompt” and the online help says:

Prompt: Gets or sets a value that will be used to set the watermark for prompts in the UI.

Figure 1 – Display attribute properties

So I set the value and looked at the output UI in Insert mode and there was no water mark so again I just had to “make it so”. So I decided to use the Ajax Control Toolkit’s TextBoxWatermark to achieve this.

Note: This example applies this to the Text and DateTime fields template, but it could also be applied to other templates that are based on Textboxes (e.g. Integer, Decimal etc.)

I’ve created a watermarked CSS class and added it to the site.css file.

.watermarked
{
     color: Silver;
}

Listing 1 – watermarked CSS class

<asp:TextBox 
    ID="TextBox1" 
    runat="server" 
    CssClass="DDTextBox"
    Text='<%# FieldValueEditString %>'> 
</asp:TextBox>
<asp:TextBoxWatermarkExtender 
    ID="TextBoxWatermarkExtender1" 
    runat="server" 
    TargetControlID="TextBox1"
    WatermarkCssClass="watermarked">
</asp:TextBoxWatermarkExtender>

Listing 2 – TextBoxWarterMarkExtender

I’ve also added the TextBoxWarterMarkExtender to both the Text_Edit.ascx and DateTime_Edit.ascx files see Listing 2 then we need to make the changes to the code behind files.

Note:  The code for the TextBoxWarterMarkExtender is the same across both field templates but there are some minor differences in the TextBox.

Here there will be major differences as we can leverage the System.Globalization namespace in the DateTime_Edit.

#region Watermarked
var display = Column.GetAttribute<DisplayAttribute>();
if (display != null && !String.IsNullOrEmpty(display.Prompt))
    TextBoxWatermarkExtender1.WatermarkText = display.Prompt;
else
    TextBoxWatermarkExtender1.Enabled = false;
#endregion

Listing 3 – Watermarked region of the Text_Edit.ascx.cs Page_Load event.

In Listing 3 in the Text_Edit field template we are just looking for the Display attribute and it’s Prompt property and if we have a prompt value then we apply it to the watermark otherwise we disable the extender.

#region Watermarked
String defaultFormat = String.Empty;
if (Column.DataTypeAttribute != null)
{
    // get format string depending on the data type
    switch (Column.DataTypeAttribute.DataType)
    {
        case DataType.Date:
            defaultFormat = DateTimeFormatInfo.CurrentInfo.ShortDatePattern;
            break;
        case DataType.DateTime:
            defaultFormat = DateTimeFormatInfo.CurrentInfo.FullDateTimePattern;
            break;
        case DataType.Time:
            defaultFormat = DateTimeFormatInfo.CurrentInfo.LongTimePattern;
            break;
    }
}
else
    defaultFormat = DateTimeFormatInfo.CurrentInfo.ShortDatePattern;

// if we have a Prompt value then override the default format string
var display = Column.GetAttribute<DisplayAttribute>();
if (display != null && !String.IsNullOrEmpty(display.Prompt))
    TextBoxWatermarkExtender1.WatermarkText = display.Prompt;
else
    TextBoxWatermarkExtender1.WatermarkText = defaultFormat;
#endregion

Listing 3 – Watermarked region of the DateTime_Edit.ascx.cs Page_Load event.

So then for the DateTime field template we could have a default watermark depending on the DateType set for the DateTime field (see Listing 3). Here in Listing 3 we are using System.Globalization to get the format string based on culture and date type (DateTime, Date or Time) but allowing the Prompt to be overridden.

Watermarked fields

Figure 1 – Watermarked fields

Obviously this could be applied to other fields that are based on TextBox, but this will do for now.

Download

Note: This sample uses Ajax Control Toolkit

Monday 15 February 2010

Grouping Fields on Details, Edit and Insert with Dynamic Data 4, VS2010 and .Net 4.0 RC1

Whilst writing my last article over the week end I noticed the new Display attribute see Figure 1 the first one that intrigued me was the GroupName parameter, so the first thing I did was add some GroupName to some metadata on a Northwind table.

Display attribute parameters

Figure 1 – Display attribute parameters

[MetadataType(typeof(OrderMetadata))]
public partial class Order
{
    internal partial class OrderMetadata
    {
        public Object OrderID { get; set; }
        public Object CustomerID { get; set; }
        public Object EmployeeID { get; set; }
        [Display(Order = 0,GroupName = "Dates")]
        public Object OrderDate { get; set; }
        [Display(Order = 1,GroupName = "Dates")]
        public Object RequiredDate { get; set; }
        [Display(Order = 2,GroupName = "Dates")]
        public Object ShippedDate { get; set; }
        [Display(Order = 4,GroupName = "Ship Info")]
        public Object ShipVia { get; set; }
        [Display(Order = 5,GroupName = "Ship Info")]
        public Object Freight { get; set; }
        [Display(Order = 3,GroupName = "Ship Info")]
        public Object ShipName { get; set; }
        [Display(Order = 6,GroupName = "Ship Info")]
        public Object ShipAddress { get; set; }
        [Display(Order = 7,GroupName = "Ship Info")]
        public Object ShipCity { get; set; }
        [Display(Order = 8,GroupName = "Ship Info")]
        public Object ShipRegion { get; set; }
        [Display(Order = 9,GroupName = "Ship Info")]
        public Object ShipPostalCode { get; set; }
        [Display(Order = 10,GroupName = "Ship Info")]
        public Object ShipCountry { get; set; }
        // Entity Ref 
        [Display(Order = 12,GroupName = "Other Info")]
        public Object Customer { get; set; }
        // Entity Ref 
        [Display(Order = 13,GroupName = "Other Info")]
        public Object Employee { get; set; }
        // Entity Set 
        [Display(Order = 14,GroupName = "Other Info")]
        public Object Order_Details { get; set; }
        // Entity Ref 
        [Display(Order = 11,GroupName = "Ship Info")]
        public Object Shipper { get; set; }
    }
}

Listing 1 – GroupName metadata.

Well when I ran the app and get Figure 2 I was a little disappointed, I’d expected that at least the field would be grouped together by group name (maybe this was not the intended use but I was determined to make it work) but better still would have been with a separator containing the group name.

GroupingBefore

Figure 2 – Orders table with GroupName metadata

So I set about “making it so” (to quote Captain Picard) the first step was to group the fields so I looked at the new EntityTemplates.

<asp:EntityTemplate runat="server" ID="EntityTemplate1">
    <ItemTemplate>
        <tr class="td">
            <td class="DDLightHeader">
                <asp:Label 
                    runat="server"
                    OnInit="Label_Init" />
            </td>
            <td>
                <asp:DynamicControl 
                    runat="server"
                    OnInit="DynamicControl_Init" />
            </td>
        </tr>
    </ItemTemplate>
</asp:EntityTemplate>

Listing 2 – Default.ascx entity template

public partial class DefaultEntityTemplate : EntityTemplateUserControl
{
    private MetaColumn currentColumn;

    protected override void OnLoad(EventArgs e)
    {
        foreach (MetaColumn column in Table.GetScaffoldColumns(Mode, ContainerType))
        {
            currentColumn = column;
            Control item = new _NamingContainer();
            EntityTemplate1.ItemTemplate.InstantiateIn(item);
            EntityTemplate1.Controls.Add(item);
        }
    }

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

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

    public class _NamingContainer : Control, INamingContainer { }
}

Listing 3 – Default.ascx.cs entity template code behind.

If you look at my old Custom PageTemplates Part 4 - Dynamic/Templated FromView sample, I implemented the ITemplate interface for generating FormView and also ListView templates dynamically, and I remember at the time David Ebbo commenting on this and how he was working on something a little more flexible and then Entity Templates were unveiled in one of the early previews; but this is considerably more flexible than my sample was. I think we will be able to extend this greatly in the future but for now I’ll be happy with making grouping work.

So the first thing I did was tweak the default entity template to order by groups.

protected override void OnLoad(EventArgs e)
{
    // get a list of groups ordered by group name
    var groupings = from t in Table.GetScaffoldColumns(Mode, ContainerType)
                    group t by t.GetAttributeOrDefault<DisplayAttribute>().GroupName into menu
                    orderby menu.Key
                    select menu.Key;

    // loop through the groups
    foreach (var groupId in groupings)
    {
        // get columns for this group
        var columns = from c in Table.GetScaffoldColumns(Mode, ContainerType)
                      where c.GetAttributeOrDefault<DisplayAttribute>().GroupName == groupId
                      orderby c.GetAttributeOrDefault<DisplayAttribute>().GetOrder()
                      select c;

        // add fields
        foreach (MetaColumn column in columns)
        {
            currentColumn = column;
            Control item = new _NamingContainer();
            EntityTemplate1.ItemTemplate.InstantiateIn(item);
            EntityTemplate1.Controls.Add(item);
        }
    }
}

Listing 4 – extended default entity template stage 1

So what I did in listing 4 was get a list of all the groups sorted by group name, and then loop through the groups getting the column for each group; then generate the groups fields. Visually this does not produce much of a difference than the initial display.

Grouping with Sort

Figure 3 – Grouping with Sort.

Now we can see the groups coming together, next we need to add the visual aspect.

A little surgery on the ascx part of the entity template is required to get this to work. In Listing 5 you can see that I have added some runat=”server” properties to the TD’s of the template.

<asp:EntityTemplate runat="server" ID="EntityTemplate1">
    <ItemTemplate>
        <tr class="td">
            <td class="DDLightHeader" runat="server">
                <asp:Label 
                    runat="server" 
                    OnInit="Label_Init" />
            </td>
            <td runat="server">
                <asp:DynamicControl 
                    runat="server" 
                    OnInit="DynamicControl_Init" />
            </td>
        </tr>
    </ItemTemplate>
</asp:EntityTemplate>

Listing 5 – modified default.ascx Entity Template.

Moving to the Default entity templates code behind in Listing 6 I have added the code to add a separator, but it will need some modification as at the moment is just a repeat of one of the columns.

protected override void OnLoad(EventArgs e)
{
    // get a list of groups ordered by group name
    var groupings = from t in Table.GetScaffoldColumns(Mode, ContainerType)
                    group t by t.GetAttributeOrDefault<DisplayAttribute>().GroupName into menu
                    orderby menu.Key
                    select menu.Key;

    // loop through the groups
    foreach (var groupId in groupings)
    {
        // get columns for this group
        var columns = from c in Table.GetScaffoldColumns(Mode, ContainerType)
                      where c.GetAttributeOrDefault<DisplayAttribute>().GroupName == groupId
                      orderby c.GetAttributeOrDefault<DisplayAttribute>().GetOrder()
                      select c;

        // add group separator
        if (!String.IsNullOrEmpty(groupId))
        {
            groupHeading = true;
            currentColumn = columns.First();
            groupName = groupId;
            Control item = new _NamingContainer();
            EntityTemplate1.ItemTemplate.InstantiateIn(item);
            EntityTemplate1.Controls.Add(item);
        }

        // add fields
        foreach (MetaColumn column in columns)
        {
            groupHeading = false;
            currentColumn = column;
            Control item = new _NamingContainer();
            EntityTemplate1.ItemTemplate.InstantiateIn(item);
            EntityTemplate1.Controls.Add(item);
        }
    }
}

Listing 6 – final version of the OnLoad handler.

For the final tweaks of the visual of the separator we will done some extra manipulation in the two Init handlers of the Label and the DynamicControl.

For the Label wee need to change the text so I have added a class field groupHeading as a Boolean which if you look in Listing 6 I’m setting to true when it is a group and false when a field.

protected void Label_Init(object sender, EventArgs e)
{
    if (!groupHeading)
    {
        Label label = (Label)sender;
        label.Text = currentColumn.DisplayName;
    }
    else
    {
        Label label = (Label)sender;
        label.Text = groupName;
        var parentCell = label.GetParentControl<HtmlTableCell>();
        parentCell.ColSpan = 2;
        parentCell.Attributes.Add("class", "DDGroupHeader");
    }
}

Listing 7 – Label_Init handler

So in Listing 7 you can see that we do the standard thing if it is a filed, but do some custom stuff if it is a group heading. I first get the parent control (see Listing 8 for source) of type HtmlTableCell (we can get this because we set it to runat=”server”). Once we have the parent cell we can manipulate it; first of all we set it’s colspan attribute to 2 and change the CSS class to "DDGroupHeader" to make it stand out.

/// <summary>
/// Gets the parent control.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="control">The control.</param>
/// <returns></returns>
public static T GetParentControl<T>(this Control control) where T : Control
{
    var parentControl = control.Parent;
    // step up through the parents till you find a control of type T
    while (parentControl != null)
    {
        var p = parentControl as T;
        if (p != null)
            return p;
        else
            parentControl = parentControl.Parent;
    }
    return null;
}

Listing 8 – GetParentControl extension method.

.DDGroupHeader
{
	font-weight: bold;
	font-style: italic;
	background-color: Silver;
}

Listing 9 – DDGroupHeader CSS snippet

The next thing is to hide the DynamicControl and it’s HtmlTableCell so the label can span both columns.  Now if we are in a group header then we hide the DynamicControl, get the parent cell and also hide it, letting the label’s cell span both rows.

protected void DynamicControl_Init(object sender, EventArgs e)
{
    DynamicControl dynamicControl = (DynamicControl)sender;
    dynamicControl.DataField = currentColumn.Name;
    if (groupHeading)
    {
        // hide Dynamic Control maybe overkill
        dynamicControl.Visible = false;
        // get the parent cell
        var parentCell = dynamicControl.GetParentControl<HtmlTableCell>();
        // hide the cell
        parentCell.Visible = false;
    }
}

Listing 10 – DynamicControl_Init handler

Note: The DynamicControl must have it’s DataField set otherwise it will throw an error.

Grouping with visual separators

Figure 4 – Grouping with visual separators.

Now we have separators working, “made so” I would think, well only partially with this version you have to repeat all the above with a few minor changed for Edit and Insert EntityTemplates, but that is in the sample.

Note: Remember this sample is with Visual Studio 2010 and .Net 4.0 RC1

Download

As always have fun coding

Sunday 14 February 2010

A New Way To Do Column Generation in Dynamic Data 4 (UPDATED)

There have been several questions on the Dynamic Data Forum saying things like IAutoFieldGenerator does not work with Details, Edit and Insert pages. This is because these page template have now moved to FormView which allows for us to have the nice new Entity Templates and this is cool; but leaves us with the issue of having to do custom column generation in two ways one for form view and one for GridView in List and ListDetails pages. So harking back to this post A Great Buried Sample in Dynamic Data Preview 4 – Dynamic Data Futures long ago in a year far far awayBig Grin

So what I am planning to do is add our own MetaModel that we can pass in a delegate to produce a custom list of columns. I am going to implement the Hide column based on page template (HideColumnInAttribute) for now.

The Custom MetaModel


So first things first lets build out custom MetaModel, the only two classes we will need to implement for our custom MetaModel are:


  MetaModel
  MetaTable

We need MetaModel because it goes away and get the other classes.

public class CustomMetaModel : MetaModel
{
    /// <summary>
    /// Delegate to allow custom column generator to be passed in.
    /// </summary>
    public delegate IEnumerable<MetaColumn> GetVisibleColumns(IEnumerable<MetaColumn> columns);

    private GetVisibleColumns _getVisdibleColumns;

    public CustomMetaModel() { }

    public CustomMetaModel(GetVisibleColumns getVisdibleColumns)
    {
        _getVisdibleColumns = getVisdibleColumns;
    }

    protected override MetaTable CreateTable(TableProvider provider)
    {
        if (_getVisdibleColumns == null)
            return new CustomMetaTable(this, provider);
        else
            return new CustomMetaTable(this, provider, _getVisdibleColumns);
    }
}

Listing 1 – Custom MetaModel class

So what are we doing here, firstly we have a delegate so we can pass in a methods to do the column generation and we are passing this in through our custom constructor. Then in the only method we are overriding we are returning the CustomMetaTable class, and passing in the delegate if it has been set.

public class CustomMetaTable : MetaTable
{
    private  CustomMetaModel.GetVisibleColumns _getVisdibleColumns;

    /// <summary>
    /// Initializes a new instance of the <see cref="CustomMetaTable"/> class.
    /// </summary>
    /// <param name="metaModel">The entity meta model.</param>
    /// <param name="tableProvider">The entity model provider.</param>
    public CustomMetaTable(MetaModel metaModel, TableProvider tableProvider) :
        base(metaModel, tableProvider) { }

    /// <summary>
    /// Initializes a new instance of the <see cref="CustomMetaTable"/> class.
    /// </summary>
    /// <param name="metaModel">The meta model.</param>
    /// <param name="tableProvider">The table provider.</param>
    /// <param name="getVisibleColumns">Delegate to get the visible columns.</param>
    public CustomMetaTable(
        MetaModel metaModel, 
        TableProvider tableProvider, 
        CustomMetaModel.GetVisibleColumns getVisibleColumns) :
        base(metaModel, tableProvider)
    {
        _getVisdibleColumns = getVisibleColumns;
    }

    protected override void Initialize()
    {
        base.Initialize();
    }

    public override IEnumerable<MetaColumn> GetScaffoldColumns(
        DataBoundControlMode mode, 
        ContainerType containerType)
    {
        if (_getVisdibleColumns == null)
            return base.GetScaffoldColumns(mode, containerType);
        else
            return _getVisdibleColumns(base.GetScaffoldColumns(mode, containerType));
    }
}

Listing 2 – Custom MetaTable class

In CustomMetaTable we have the default constructor and a custom constructor again we passing the delegate into the custom constructor. Now in the only method we are overriding we either call the base GetScaffoldColumns or our delegate if is has been set. And that’s it as far as the Meta Classes are concerned.

The Attribute

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class HideColumnInAttribute : Attribute
{
    public PageTemplate PageTemplate { get; private set; }

    public HideColumnInAttribute() { }

    public HideColumnInAttribute(PageTemplate lookupTable)
    {
        PageTemplate = lookupTable;
    }
}

Listing 3 – HideColumnInAttribute

Listing 3 is the HideColumnIn attribute see Dynamic Data - Hiding Columns in selected PageTemplates for details on this attribute.

public static class ControlExtensionMethods
{
    // "~/DynamicData/PageTemplates/List.aspx"
    private const String extension = ".aspx";

    /// <summary>
    /// Gets the page template from the page.
    /// </summary>
    /// <param name="page">The page.</param>
    /// <returns></returns>
    public static PageTemplate GetPageTemplate(this Page page)
    {
        try
        {
            return (PageTemplate)Enum.Parse(typeof(PageTemplate),
                page.RouteData.Values["action"].ToString());
        }
        catch (ArgumentException)
        {
            return PageTemplate.Unknown;
        }
    }
}

Listing 4 – GetPageTemplate extension method.

Updated: Here the GetPageTemplate extension method has been updated thanks to beeps4848 from the DD forum see this thread How to cast integer values as an array of enum values? where he makes this cool suggestion of using the RoutData to get the action name, so maybe the attribute should now be HideColumnInAction ot the Enum ActionName (PageActions has been used).
[Flags]
public enum PageTemplate
{
    // standard page templates
    Details         = 0x01,
    Edit            = 0x02,
    Insert          = 0x04,
    List            = 0x08,
    ListDetails     = 0x10,
    // unknown page templates
    Unknown         = 0xff,
}

Listing 5 – PageTemplate enum.

In Listing 4 we have the new improved GetPageTemplate extension method now you don’t have to change each page to inherit DynamicPage you can just call the Page.GetPageTemplate() to find out which page you are on. it required the PageTemplate enum in listing 5.

The Delegate Methods

public static IEnumerable<MetaColumn> GetVisibleColumns(IEnumerable<MetaColumn> columns)
{
    var visibleColumns = from c in columns
                         where IsShown(c)
                         select c;
    return visibleColumns;
}

public static Boolean IsShown(MetaColumn column)
{
    // need to get the current page template
    var page = (System.Web.UI.Page)System.Web.HttpContext.Current.CurrentHandler;
    var pageTemplate = page.GetPageTemplate();

    var hideIn = column.GetAttribute<HideColumnInAttribute>();
    if (hideIn != null)
        return !((hideIn.PageTemplate & pageTemplate) == pageTemplate);

    return true;
} 

Listing 6 – Column generator methods

Now we need to supply our own column generator methods, in Listing 6 we have two methods the first GetVisibleColumns (and the name does not need to be the same as the Delegate) is the one we pass into the MetaModel, and the second IsHidden is where we test to see if the column should be hidden or not.

Adding To Web Application

Now we need to put these into our sample web application.

 public class Global : System.Web.HttpApplication
 {
     private static MetaModel s_defaultModel = new CustomMetaModel(GetVisibleColumns);
     public static MetaModel DefaultModel
     {
         get { return s_defaultModel; }
     }
    // other code ...
}

Listing 7 – Adding to Global.asax

So all we have to do is change the default value in Global.asax from

private static MetaModel s_defaultModel = new MetaModel();

to

private static MetaModel s_defaultModel = new CustomMetaModel(GetVisibleColumns);

Now all column generation throughout the site is handled by the GetVisibleColumns method from Listing 6.

[MetadataType(typeof(OrderMetadata))]
public partial class Order
{
    internal partial class OrderMetadata
    {
        public Object OrderID { get; set; }
        public Object CustomerID { get; set; }
        public Object EmployeeID { get; set; }
        public Object OrderDate { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object RequiredDate { get; set; }
        public Object ShippedDate { get; set; }
        public Object ShipVia { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object Freight { get; set; }
        public Object ShipName { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipAddress { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipCity { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipRegion { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipPostalCode { get; set; }
        [HideColumnIn(PageTemplate.List)]
        public Object ShipCountry { get; set; }
        // Entity Ref 
        public Object Customer { get; set; }
        // Entity Ref 
        public Object Employee { get; set; }
        // Entity Set 
        public Object Order_Details { get; set; }
        // Entity Ref 
        public Object Shipper { get; set; }
    }
}

Listing 8 – sample metadata.

Download

Note: The sample is for Visual Studio 2010 RC

Happy Coding

Sunday 7 February 2010

A Slightly Improved Password Field Template

I know my previous password field template was just about functional, so I have improved it slightly now you will have to enter the password twice in password mode and they will have to match. And in Read-Only mode it just displays ********** or as many ‘*’ as you like.Happy

<asp:Literal runat="server" ID="Literal1" Text="**********" />

Listing 1 - Password.ascx page and the code behind is even less interesting.

<%@ Control 
    Language="C#" 
    CodeFile="Password_Edit.ascx.cs" 
    Inherits="Password_EditField" %>
    
<asp:TextBox 
    ID="TextBox1" 
    runat="server" 
    TextMode="Password" 
    CssClass="droplist">
</asp:TextBox>
<asp:HiddenField 
    ID="HiddenField1" 
    runat="server" 
    Value='<%# GetValue() %>' />
<asp:CompareValidator 
    ID="CompareValidator1" 
    runat="server" 
    ControlToValidate="TextBox1" 
    ControlToCompare="TextBox2" 
    EnableClientScript="true"
    SetFocusOnError="True"
    Text="*" />
<asp:RequiredFieldValidator 
    runat="server" 
    ID="RequiredFieldValidator1" 
    CssClass="droplist"
    ControlToValidate="TextBox1" 
    Display="Dynamic" 
    Enabled="false" 
    Text="*" />
<asp:RegularExpressionValidator 
    runat="server" 
    ID="RegularExpressionValidator1" 
    CssClass="droplist"
    ControlToValidate="TextBox1" 
    Display="Dynamic" 
    Enabled="false" 
    Text="*" />
<asp:DynamicValidator 
    runat="server" 
    ID="DynamicValidator1" 
    CssClass="droplist" 
    ControlToValidate="TextBox1"
    Display="Dynamic" 
    Text="*" />
<br />
<asp:TextBox 
    ID="TextBox2" 
    runat="server" 
    TextMode="Password" 
    CssClass="droplist">
</asp:TextBox>

Listing 2 – Password_Edit.ascx

Password

Figure 1 – Password field template.

From Listing 2 and Figure 1 you can see all I’ve done is add a CompareValidator and an extra TextBox to the page. I have configured the CompareValidators two properties to:

ControlToValidate="TextBox1" 
ControlToCompare="TextBox2"

and enabled client script via the EnableClientScript property.

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

public partial class Password_EditField : System.Web.DynamicData.FieldTemplateUserControl
{
    protected void Page_Load(object sender, EventArgs e)
    {
        TextBox1.MaxLength = Column.MaxLength;
        if (Column.MaxLength < 20)
            TextBox1.Columns = Column.MaxLength;
        TextBox1.ToolTip = Column.Description;

        CompareValidator1.ErrorMessage = "passwords must match";

        if (Mode == DataBoundControlMode.Insert)
            SetUpValidator(RequiredFieldValidator1);

        SetUpValidator(RegularExpressionValidator1);
        SetUpValidator(DynamicValidator1);
    }

    protected override void ExtractValues(IOrderedDictionary dictionary)
    {
        var original = dictionary[Column.Name];
        // make sure we have some text
        if (TextBox1.Text.Trim().Length > 0)
            dictionary[Column.Name] = ConvertEditedValue(TextBox1.Text);
        else if (HiddenField1.Value.Length > 0)
            dictionary[Column.Name] = HiddenField1.Value;

    }

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

Listing 3 – Password_Edit.ascx.cs

In Listing 3 we can see that I have set the ErrorMessage property to “passwords must match” as if we use the SetUpValidator it just disables the validator so this will require a little extra work if you want it localised. Also you will note the line:

Updated: I’ve made some changes to deal with the issue of RequiredFieldValidator I’ve added a HiddenField to hold the current password and if no password has been entered during an edit and the field is required I check to see if we have a value in the hidden field (as a validation error for required field will be thrown even if no RequiredFieldValidator is setup or present)
protected override void ExtractValues(IOrderedDictionary dictionary)
{
    var original = dictionary[Column.Name];
    // make sure we have some text
    if (TextBox1.Text.Trim().Length > 0)
        dictionary[Column.Name] = ConvertEditedValue(TextBox1.Text);
}

In the ExtractValues method that check to see if the TextBox is empty ‘if (TextBox1.Text.Trim().Length > 0)’ if it is empty we simply return no value which means the value stays the same.

The only issue is that if you add text to the second TextBox (without entering any text into the first) then validation fails in that no error is flagged, nut no values is saved, so in this case the password would remain the same. I have looked at this and am writing an extended CompareValidator to a) fix this inconsistency and b) give a server side validation event if required.

I wont bother with a download this time as you have all the code in Listings 2 & 3 and you have all you need for the Password.ascx in Listing 1.

Happy Coding

Tuesday 2 February 2010

Applying CSS Styling via Metadata

In answer to a post on the ASP.Net Dynamic Data Forum from Luigi here I though I should post my solution. The basic issue was dealt with here by Rick Anderson I decided to implement a version that used Attributes and a simple extension method to apply the attribute values:

Firstly here is the attribute, it just handles one class name.

/// <summary>
/// Used to apply css classes to the cell 
/// that encloses a field template.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CssAttribute : Attribute
{
    public String Class { get; private set; }

    public CssAttribute()
    {
    }

    public CssAttribute(String css)
    {
        Class = css;
    }
}

Listing 1 – CSS Attribute

There are two extension methods I use to implement the CssAttribute, the first Listing 2 is used in the Field Template  is a helper that I use in many other places; I use it to get the first parent control of a Type DetailsView GridView etc. here I’m use it to get the parent DataControlFieldCell or TD/cell of the table, obviously if you are using FormView or ListView you will need to specify the relevant container control.

/// <summary>
/// Gets the container control.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="control">The control.</param>
/// <returns></returns>
public static T GetContainerControl<T>(this Control control) where T : Control
{
    // get current parent
    var parentControl = control.Parent;
    while (parentControl != null)
    {
        var p = parentControl as T;
        if (p != null)
        {
            // return found control
            return p;
        }
        else
        {
            // get next parent
            parentControl = parentControl.Parent;
        }
    }
    // if no control found return null
    return null;
}

Listing 2 – GetContainerControl

The next Listing 3 actually applies the attribute by first calling the GetParentControl helper to get the containing cell/TD then extracts the class attribute and applies the new class either by appending or assigning.

/// <summary>
/// Applies the CSS.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="Column">The column.</param>
public static void ApplyCss(this FieldTemplateUserControl control, MetaColumn Column)
{
    // get the attribute
    var css = Column.GetAttribute<CssAttribute>();
    // get parent control
    var parentCell = control.GetContainerControl<DataControlFieldCell>();

    // make sure we have the attribute and parent control
    if (css != null && parentCell != null)
    {
        // test for the presence of a class attribute on the cell
        var cssClass = parentCell.Attributes["class"];
        if (cssClass != null)
        {
            // add the extra class
            cssClass += " " + css.Class;
        }
        else
        {
            // assign the class
            parentCell.Attributes.Add("class", css.Class);
        }
    }
}

Listing 3 – ApplyCss method

You will also notice the GetAttribute extension method which I wont go into as you can find details on this site here Writing Attributes and Extension Methods for Dynamic Data

Some sample Metadata showing adding cantering to a single column:

[MetadataType(typeof(CategoryMetadata))]
public partial class Category
{
    internal class CategoryMetadata
    {
        public Object CategoryID { get; set; }
        [Css("CenterCell")]
        public Object CategoryName { get; set; }
        public Object Description { get; set; }
        public Object Picture { get; set; }
        // Entity Set 
        public Object Products { get; set; }
    }
}

Listing 4 – CssAttribute applied

Appled Css attribute

Figure 1 – applied CSS attribute

Download

Happy coding Big Grin