Thursday 31 March 2011

Custom Entity Templates – Dynamic Data 4

In Dynamic Data 4 we have Entity Templates these are great for customizing layout of tables (see Walkthrough: Customizing Table Layout Using Entity Templates).  At the moment you will have to create a Custom Entity template for each table, but not only that you will need to create a set of three (Template, Template_Edit and Template_Insert) and that can be useful to have different layout for each view.

But what if you want something different from the Default, (see Grouping Fields on Details, Edit and Insert with Dynamic Data 4, VS2010 and .Net 4.0 RC1 my first custom Entity Template which then had to replace the Default and also had to have all three defined) but not on all your tables just on a selected few?

I was inspired by Brian Pritchard’s post on the forum: How to reduce code duplication when using Entity Template's for only having to create a single template that would switch between Read-Only, Edit and Insert modes.

I want to go a little further:

  1. Override the Default Entity Template with some sort of UIHint attribute.
  2. An only have to specify one template to keep things DRY
  3. Detect if an Edit or Insert version of a template exists and use that.

So now we know out goal, let’s layout the ingredients for this recipe:

  1. An attribute to allow us to change the default Entity Template for any given Table.
  2. Custom entity factory to allow us to override normal entity behaviour like Brian Pritchard’s.
  3. A Custom dynamic Entity Template.

The Attribute

I thought I would be able to use UIHint at class level but alas we have to hand craft our own, I want some of the same feature of UIHint specifically the Control Parameters collection so that we can pass extra information into our custom Entity Templates with out creating a plethora extra attribute each specific to it’s one Entity Template. (I’ve used the Control Parameters collection before in Dynamic Data Custom Field Template – Values List, Take 2.

The Attribute is relatively straight forward, the only complication is the BuildControlParametersDictionary method which takes the Object array passed in using the params key word into a Key, Value Dictionary with some validation. Note we have also set this attribute to be only useable at Class level.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class EntityUIHintAttribute : Attribute
{
    private IDictionary _controlParameters;

    public IDictionary ControlParameters
    {
        get { return this._controlParameters; }
    }

    /// 
    /// Gets or sets the UI hint.
    /// 
    /// The UI hint.
    public String UIHint { get; private set; }

    public EntityUIHintAttribute(string uiHint) : this(uiHint, new object[0]) { }

    public EntityUIHintAttribute(string uiHint, params object[] controlParameters)
    {
        UIHint = uiHint;
        _controlParameters = BuildControlParametersDictionary(controlParameters);
    }

    public override object TypeId
    {
        get { return this; }
    }

    private IDictionary BuildControlParametersDictionary(object[] objArray)
    {
        IDictionary dictionary = new Dictionary();
        if ((objArray != null) && (objArray.Length != 0))
        {
            if ((objArray.Length % 2) != 0)
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Need even number of control parameters.", new object[0]));

            for (int i = 0; i < objArray.Length; i += 2)
            {
                object obj2 = objArray[i];
                object obj3 = objArray[i + 1];
                if (obj2 == null)
                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Control parameter key is null.", new object[] { i }));

                string key = obj2 as string;
                if (key == null)
                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Control parameter key is not a string.", new object[] { i, objArray[i].ToString() }));

                if (dictionary.ContainsKey(key))
                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Control parameter key occurs more than once.", new object[] { i, key }));

                dictionary[key] = obj3;
            }
        }
        return dictionary;
    }
}

Listing 1 – EntityUIHintAttribute

Now we need a method to use this attribute to change the default Entity Template, this factory need to do two things:

  1. Change the Default Entity Template based on out new EntityUIHint attribute.
  2. Intercept the template mode so that single Entity Template can be used with out having to have three versions.
public class AdvancedEntityTemplateFactory : System.Web.DynamicData.EntityTemplateFactory
{
    public override string BuildEntityTemplateVirtualPath(string templateName, DataBoundControlMode mode)
    {
        var path = base.BuildEntityTemplateVirtualPath(templateName, mode);
        var editPath = base.BuildEntityTemplateVirtualPath(templateName, DataBoundControlMode.Edit);;
        var defaultPath = base.BuildEntityTemplateVirtualPath(templateName, DataBoundControlMode.ReadOnly); ;

        if (File.Exists(HttpContext.Current.Server.MapPath(path)))
            return path;

        if (mode == DataBoundControlMode.Insert && File.Exists(HttpContext.Current.Server.MapPath(editPath)))
            return editPath;

        if (mode != DataBoundControlMode.ReadOnly && File.Exists(HttpContext.Current.Server.MapPath(defaultPath)))
            return defaultPath;

        return path;
    }

    public override EntityTemplateUserControl CreateEntityTemplate(MetaTable table, DataBoundControlMode mode, string uiHint)
    {
        var et = table.GetAttribute();
        if (et != null && !String.IsNullOrEmpty(et.UIHint))
            return base.CreateEntityTemplate(table, mode, et.UIHint);

        return base.CreateEntityTemplate(table, mode, uiHint);
    }

    public override string GetEntityTemplateVirtualPath(MetaTable table, DataBoundControlMode mode, string uiHint)
    {
        var et = table.GetAttribute();
        if (et != null && !String.IsNullOrEmpty(et.UIHint))
            return base.GetEntityTemplateVirtualPath(table, mode, et.UIHint);

        return base.GetEntityTemplateVirtualPath(table, mode, uiHint);
    }
}

Listing 2 – AdvancedEntityTemplateFactory

Listing 1 shows us out AdvancedEntityTemplateFactory, we fulfil task 1. in the methods CreateEntityTemplate and GetEntityTemplateVirtualPath where we check for the presence of a EntityUIHintAttribute and if we find one then set the name of the template to the UIHint property.

Task 2. is dealt with in the BuildEntityTemplateVirtualPath where we check to see if the file exists, if so we just return the path as is, otherwise we strip out the  _Edit or _Insert from the path and return.

The last thing we need is to wire up the AdvancedEntityTemplateFactory in Global.asax.cs

DefaultModel.EntityTemplateFactory = new AdvancedEntityTemplateFactory();

just before the RegisterContext in RegisterRoutes method.

The Custom Entity Template

Multi Column Entity Template

Figure 1 – Multi Column Entity Template

This entity template will be a multi column temp[late designed to give you a little more screen for your money Big Grin I have decided to pass the main parameters in via the EntityUIHint attribute’s Control Parameters, in Figure 2 you can see them in pairs.

Control Parameters

Figure 2 – Control Parameters

“Columns”, 3 sets the number of column the MultiColumn entity template will show.

The next two pairs are the Title and Field CSS classes.

I’ve also decided to add the ability for some columns to span more then one cell in the table.

[AttributeUsage(AttributeTargets.Property)]
public class MultiColumnAttribute : Attribute
{
    /// 
    /// Gets or sets the column span.
    /// 
    /// The column span.
    public int ColumnSpan { get; private set; }

    public static MultiColumnAttribute Default = new MultiColumnAttribute();

    public MultiColumnAttribute() 
    { 
        ColumnSpan = 1;
    }

    public MultiColumnAttribute(int columnSpan)
    {
        ColumnSpan = columnSpan;
    }
}

Listing 3 – MultiColumnAttribute

The use of Default for when we use the DefaultIfEmpty method in Linq (see my Writing Attributes and Extension Methods for Dynamic Data more info) this allows us to get an attribute even if one is not specified so now with thi sline of code

var totalNoOfCells = metaColumns.Select(c => c.GetAttributeOrDefault<MultiColumnAttribute>().ColumnSpan).Sum();

we can get the total number of cell required with some columns having a span of more than on column see Figure 1.

Finally our Multi Column entity template is completed in Listing 4

public partial class MultiColumnEntityTemplate : System.Web.DynamicData.EntityTemplateUserControl
{
    private const string COLUMNS = "Columns";
    private const string TITLE_CSS_CLASS = "TitleCssClass";
    private const string FIELD_CSS_CLASS = "FieldCssClass";

    protected override void OnLoad(EventArgs e)
    {
        // get columns from table
        var metaColumns = Table.GetScaffoldColumns(Mode, ContainerType).ToList();

        // do not render any HTML table if there are no columns returned
        if (metaColumns.Count == 0)
            return;

        // default the HTML table columns and CSS class names
        int columns = 2;
        String titleCssClass = String.Empty;
        String fieldCssClass = String.Empty;

        // Get the CssClass for the title & Field from the attribute
        var entityUHint = Table.GetAttribute();
        if (entityUHint != null)
        {
            if (entityUHint.ControlParameters.Keys.Contains(COLUMNS))
                columns = (int)entityUHint.ControlParameters[COLUMNS];
            if (entityUHint.ControlParameters.Keys.Contains(TITLE_CSS_CLASS))
                titleCssClass = entityUHint.ControlParameters[TITLE_CSS_CLASS].ToString();
            if (entityUHint.ControlParameters.Keys.Contains(FIELD_CSS_CLASS))
                fieldCssClass = entityUHint.ControlParameters[FIELD_CSS_CLASS].ToString();
        }

        // start in the left column
        int col = 0;

        // create the header & data cells
        var headerRow = new HtmlTableRow();
        if (!String.IsNullOrEmpty(titleCssClass))
            headerRow.Attributes.Add("class", titleCssClass);
        var dataRow = new HtmlTableRow();
        if (!String.IsNullOrEmpty(fieldCssClass))
            dataRow.Attributes.Add("class", fieldCssClass);

        // step through each of the columns to be added to the table
        foreach (var metaColumn in metaColumns)
        {
            // get the MultiColumn attribute for the column
            var multiColumn = metaColumn.GetAttributeOrDefault();
            if (multiColumn.ColumnSpan > columns)
                throw new InvalidOperationException(String.Format("MultiColumn attribute specifies that this 
                    field occupies {0} columns, but the EntityUIHint attribute for the class only allocates {1} 
                    columns in the HTML table.", multiColumn.ColumnSpan, columns));

            // check if there are sufficient columns left in the current row
            if (col + multiColumn.ColumnSpan > columns)
            {
                // save this header row
                this.Controls.Add(headerRow);
                headerRow = new HtmlTableRow();
                if (!String.IsNullOrEmpty(titleCssClass))
                    headerRow.Attributes.Add("class", titleCssClass);

                // save this data row
                this.Controls.Add(dataRow);
                dataRow = new HtmlTableRow();
                if (!String.IsNullOrEmpty(fieldCssClass))
                    dataRow.Attributes.Add("class", fieldCssClass);

                // need to start a new row
                col = 0;
            }

            // add the header cell
            var th = new HtmlTableCell();
            var label = new Label();
            label.Text = metaColumn.DisplayName;
            //if (Mode != System.Web.UI.WebControls.DataBoundControlMode.ReadOnly)
            //    label.PreRender += Label_PreRender;

            th.InnerText = metaColumn.DisplayName;
            if (multiColumn.ColumnSpan > 1)
                th.ColSpan = multiColumn.ColumnSpan;
            headerRow.Cells.Add(th);

            // add the data cell
            var td = new HtmlTableCell();
            var dynamicControl = new DynamicControl(Mode);
            dynamicControl.DataField = metaColumn.Name;
            dynamicControl.ValidationGroup = this.ValidationGroup;

            td.Controls.Add(dynamicControl);
            if (multiColumn.ColumnSpan > 1)
                td.ColSpan = multiColumn.ColumnSpan;
            dataRow.Cells.Add(td);

            // record how many columns we have used
            col += multiColumn.ColumnSpan;
        }
        this.Controls.Add(headerRow);
        this.Controls.Add(dataRow);
    }
}

Listing 4 – MultiColumnEntityTemplate

Updated: MultiColumnEntityTemplate updated by Phil Wigglesworth who kindly found a bug and fixed it, the bug was shown when there were no enough columns left on a row, this caused a crash. So thanks again to Phil.
Also we will need some styles to make out new Multi Column entity template look good.

TR.SmallTitle
{
    background-color: #F7F7FF;
}
TR.SmallTitle TD
{
    font-size: 0.8em !important;
    font-weight: bold;
    background-color: #F7F7FF;
    padding: 2px !important;
}

Listing 5 – CSS styles

Hope this expand your use of Entity Templates.

P.S. I’ll do an updated version of the Grouping entity template.

Download

17 comments:

Anonymous said...

Hi Steve,

great job and for someone about to embark on a large project where i'm the architect recommending DD your contributions are pure gold. :-)
John, Perth, Australia

Stephen J. Naughton said...

Thanks :)

Steve

Anonymous said...

Hi Steve,
This is a great and useful control but I have a problem.
The customs EntityTemplate doesn't render it when I use your MultiColumn

thanks for your attention

Stephen J. Naughton said...

Hi there, does the sample work?

Steve

Anonymous said...

Hi Steve,
Yes, your sample and the page in my project it works fine. The problem is when I try to include an Entity Template for the table. Simply doesn't render my custom entity template.
Thanks
Joe

Stephen J. Naughton said...

Right got you I will look into it tomorrow and post an update.

Steve

Stephen J. Naughton said...

Hi Joe, fixed that :) my bad hope that sorts it out for you, ti was the code for making templates a single template not a multiple template.

Steve

Joe said...

Hi Steve, I fixed the problem, my custom entity template now inherit from your MultiColumn Template, it works fine :)

thanks
Joe

Jay said...

Great article. I'm glad you're pushing the realm of what is possible with Dynamic Data.

Out of curiosity could Dynamic Data framework be extended to customize the order of fields that are rendered on templates such as Details, Insert, and Edit? Would be interested in getting the order of fields from a database so client can reorder fields based on their needs.

Stephen J. Naughton said...

Yes it does do this (if I'm getting what you mean) out of the boc you use the DisplayAttribute in your metadata using the Order=# property. I have added a similar thing for Filters as I thought there would be a need to having filters appear in a different order to fields.

Steve.

Fla said...

Great job, but the url for download sample.

Thanks

Fla

Stephen J. Naughton said...

Hi Fla, yes the download is a bummer but Live.com changed my SkyDrive to make it better now ALL my links are down and it's too much get all working again :(

Steve

P.S. see my post from 23rd June 2011 my public folder with all download is is:
https://skydrive.live.com/?wa=wsignin1.0&cid=96845e7b0fac1eed#cid=96845E7B0FAC1EED&id=96845E7B0FAC1EED%21112

Anonymous said...

Great!!!!!!!!!!!!!! Thank you Stephen for this job!!!!

Unknown said...

Hi Stephen, I have downloaded the solution and made the assemblies part of my project. I tested by modifying the global file and as well my entity class file for multi column hint as described. However, I am not getting the 3 columns that I hinted for the entity. Is there anything else that is needed for making the display render multi column?

Regards,
Sudhakar

Unknown said...

Hi Steve, great work. I tried to use the assemblies as is in my project. I modified two files in my project, one entity class itself and the other global aspx file for multi column as described in the article. I am not getting my entity render multi column format as hinted for the entity. Did I miss anything? Could you please let me know?

Regards, Sudhakar

Stephen J. Naughton said...

hi Sudhakar, please try setting up and running the project there may be fixes in there that the article does not mention.

https://skydrive.live.com/?wa=wsignin1.0&cid=96845e7b0fac1eed#cid=96845E7B0FAC1EED&id=96845E7B0FAC1EED%21112

Steve

Unknown said...

Hi Stephen, Thank you for putting some exceptionally good examples for aiding UI automation. I could make use your latest HideColumns and multicolumn solutions. Could you please direct me to the link where you posted on how to center all the displays( grid, form and labels)? I had to use JavaScript extensions for getting date calendar for date columns. Is there any native way of doing it? Also, I would like to get help on any UI beautification examples that you might have already posted.

Thanks in advance,

Best Regards,
Sudhakar