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
17 comments:
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
Thanks :)
Steve
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
Hi there, does the sample work?
Steve
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
Right got you I will look into it tomorrow and post an update.
Steve
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
Hi Steve, I fixed the problem, my custom entity template now inherit from your MultiColumn Template, it works fine :)
thanks
Joe
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.
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.
Great job, but the url for download sample.
Thanks
Fla
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
Great!!!!!!!!!!!!!! Thank you Stephen for this job!!!!
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
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
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
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
Post a Comment