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:
- Override the Default Entity Template with some sort of UIHint attribute.
- An only have to specify one template to keep things DRY
- 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:
- An attribute to allow us to change the default Entity Template for any given Table.
- Custom entity factory to allow us to override normal entity behaviour like Brian Pritchard’s.
- 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 IDictionaryBuildControlParametersDictionary(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:
- Change the Default Entity Template based on out new EntityUIHint attribute.
- 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
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 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.
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
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