MVC has kept me busy lately. While I like MVC overall, the "newcomer" doesn't always stack up all that well against WebForms, but does offer a great new approach to web programming. I'll add occasional comments as I run into curious things with MVC.
I'm extensively using Templated Helpers in my MVC projects and find them excellent replacements for the "missing" WebForms controls. They do have some shortcomings especially when compared to the feature-rich, customizable WebForms controls. They are not feature-rich and not customizable! It is quite difficult to even pass rudimentary parameters to a template. So I set out to change that and I have found a few ways to tackle the problem.
The first (shown here) is using the UIHintAttribute's ControlParameters property. Researching this approach, I only found this, which indicated a dead-end. But my skeptic nature prevailed and I kept digging and found a simple approach to use ControlParameters.
First, I register my own metadata provider in Global.asax.cs:
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelMetadataProviders.Current = new SoftelvdmDataAnnotationsModelMetadataProvider();
} Using the ControlParameters is done as follows, as part of the UIHintAttribute on your data model. These 3 properties use "Text" and "ShowLink" parameters.
public class HomeModel {
[UIHint("Template", "MVC", "Text", "This is the text parameter for field 1")]
public string Field1 { get; set; }
[UIHint("Template", "MVC", "ShowLink", true)]
public string Field2 { get; set; }
[UIHint("Template", "MVC", "Text", "This is the text parameter for field 3", "ShowLink", true)]
public string Field3 { get; set; }
} Finally, in your templated helper, extract the desired parameter like this:
// get the model metadata
SoftelvdmDataAnnotationsModelMetadata mmd = (SoftelvdmDataAnnotationsModelMetadata)htmlHelper.ViewData.ModelMetadata;
// if we have a ShowLink attribute, add a link
bool showLink;
if (mmd.TryGetTemplateControlParameter<bool>("ShowLink", out showLink) && showLink)
sb.Append(" <a href='http://www.softelvdm.com' target='_blank'>Link</a>"); ControlParameters are limited in that they can only accept (compile-time) constant values, in a somewhat unusual syntax, but they do allow simple vales (true/false, enumerated values, etc.), so your templated helper can behave slightly differently based on parameters. This allows you to combine related functionality, usually found in individual templates (with lots of code duplication), into one single template.
Download a complete, simple sample project (for VS2010).
Below is the actual implementation of the metadata provider, which just extends the default provider and extracts the ControlParameters. As time permits, I'll show a few other (better) ways to provide an even better mechanism to provide parameters to templates.
// This is essentially like the original DataAnnotationsModelMetadataProvider, with the addition of the ControlParameters handling
// There are probably more elegant ways to do this - it's a sample :-)
public class SoftelvdmDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider {
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) {
List<Attribute> attributeList = new List<Attribute>(attributes);
DisplayColumnAttribute displayColumnAttribute = attributeList.OfType<DisplayColumnAttribute>().FirstOrDefault();
SoftelvdmDataAnnotationsModelMetadata result = new SoftelvdmDataAnnotationsModelMetadata(this, containerType, modelAccessor, modelType, propertyName, displayColumnAttribute);
// Do [HiddenInput] before [UIHint], so you can override the template hint
HiddenInputAttribute hiddenInputAttribute = attributeList.OfType<HiddenInputAttribute>().FirstOrDefault();
if (hiddenInputAttribute != null) {
result.TemplateHint = "HiddenInput";
result.HideSurroundingHtml = !hiddenInputAttribute.DisplayValue;
}
// We prefer [UIHint("...", PresentationLayer = "MVC")] but will fall back to [UIHint("...")]
IEnumerable<UIHintAttribute> uiHintAttributes = attributeList.OfType<UIHintAttribute>();
UIHintAttribute uiHintAttribute = uiHintAttributes.FirstOrDefault(a => String.Equals(a.PresentationLayer, "MVC", StringComparison.OrdinalIgnoreCase))
?? uiHintAttributes.FirstOrDefault(a => String.IsNullOrEmpty(a.PresentationLayer));
if (uiHintAttribute != null) {
result.TemplateHint = uiHintAttribute.UIHint;
result.TemplateControlParameters = uiHintAttribute.ControlParameters; //<<<<<ADDED THIS
}
DataTypeAttribute dataTypeAttribute = attributeList.OfType<DataTypeAttribute>().FirstOrDefault();
if (dataTypeAttribute != null) {
result.DataTypeName = dataTypeAttribute.GetDataTypeName();
}
ReadOnlyAttribute readOnlyAttribute = attributeList.OfType<ReadOnlyAttribute>().FirstOrDefault();
if (readOnlyAttribute != null) {
result.IsReadOnly = readOnlyAttribute.IsReadOnly;
}
DisplayFormatAttribute displayFormatAttribute = attributeList.OfType<DisplayFormatAttribute>().FirstOrDefault();
if (displayFormatAttribute == null && dataTypeAttribute != null) {
displayFormatAttribute = dataTypeAttribute.DisplayFormat;
}
if (displayFormatAttribute != null) {
result.NullDisplayText = displayFormatAttribute.NullDisplayText;
result.DisplayFormatString = displayFormatAttribute.DataFormatString;
result.ConvertEmptyStringToNull = displayFormatAttribute.ConvertEmptyStringToNull;
if (displayFormatAttribute.ApplyFormatInEditMode) {
result.EditFormatString = displayFormatAttribute.DataFormatString;
}
}
ScaffoldColumnAttribute scaffoldColumnAttribute = attributeList.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
if (scaffoldColumnAttribute != null) {
result.ShowForDisplay = result.ShowForEdit = scaffoldColumnAttribute.Scaffold;
}
DisplayNameAttribute displayNameAttribute = attributeList.OfType<DisplayNameAttribute>().FirstOrDefault();
if (displayNameAttribute != null) {
result.DisplayName = displayNameAttribute.DisplayName;
}
RequiredAttribute requiredAttribute = attributeList.OfType<RequiredAttribute>().FirstOrDefault();
if (requiredAttribute != null) {
result.IsRequired = true;
}
return result;
}
}
public class SoftelvdmDataAnnotationsModelMetadata : DataAnnotationsModelMetadata {
public SoftelvdmDataAnnotationsModelMetadata(SoftelvdmDataAnnotationsModelMetadataProvider provider, Type containerType,
Func<object> modelAccessor, Type modelType, string propertyName,
DisplayColumnAttribute displayColumnAttribute)
: base( provider, containerType, modelAccessor, modelType, propertyName, displayColumnAttribute) {
}
public IDictionary<string, object> TemplateControlParameters { get; internal set; }
public TYPE GetTemplateControlParameter<TYPE>(string name) {
TYPE val;
if (!TryGetTemplateControlParameter<TYPE>(name, out val))
throw new ApplicationException("Missing UIHint ControlParameter");
return val;
}
public bool TryGetTemplateControlParameter<TYPE>(string name, out TYPE val) {
val = default(TYPE);
object o;
try {
o = TemplateControlParameters[name];
val = (TYPE)o;
} catch (Exception) {
return false;
}
return true;
}
}
Loading
Thank you very much for this post! Exactly what I was looking for.
I'm using this technique to add a classname to the css to set the toolbar to be used by a jQuery WYSIWYG editor.
[DataType(DataType.Html)] [UIHint("WYSIWYG", null, "toolbar", "full")] [DisplayName("Xhtml")] public string Xhtml { get; set; }And in Views\Shared\EditorTemplates\WYSIWYG.cshtml I have this:
@{ // get the model metadata SoftelvdmDataAnnotationsModelMetadata modelMetadata = (SoftelvdmDataAnnotationsModelMetadata)ViewData.ModelMetadata; // if we have a Text attribute, add it string toolbar; modelMetadata.TryGetTemplateControlParameter<string>("toolbar", out toolbar); if (string.IsNullOrEmpty(toolbar)) { toolbar = "default"; } } @Html.TextArea("" , ViewData.TemplateInfo.FormattedModelValue.ToString() , 10, 48 , new { @class = "editor-html wysiwyg toolbar-" + toolbar } )Output:
public static T GetUiHintParam<T>( this ModelMetadata modelMetadata, string paramName, T defaultValue= default(T)) { var mm=modelMetadata as SoftelvdmDataAnnotationsModelMetadata; if (mm == null) throw new InvalidCastException( "ModelMetadataProviders.Current is not SoftelvdmDataAnnotationsModelMetadataProvider."); T value; return mm.TryGetTemplateControlParameter(paramName, out value) ?value :defaultValue; } }Thanks for this post, it got be going in the right direction.
I was having problems with the above working with the latest data anotations with the newer style Display attribute. Being too lazy to figure out how to modify your code, I just removed the guts of it and added the TemplateControlParameters to the AdditionalValues dictionary of the ModelMetadata class. I then pull them back out in my Template. Seems incredibly lazy, but it works, and I don't mess with any of the built in stuff.
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { ModelMetadata metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); List<Attribute> attributeList = new List<Attribute>(attributes); // We prefer [UIHint("...", PresentationLayer = "MVC")] but will fall back to [UIHint("...")] IEnumerable<UIHintAttribute> uiHintAttributes = attributeList.OfType<UIHintAttribute>(); UIHintAttribute uiHintAttribute = uiHintAttributes.FirstOrDefault(a => String.Equals(a.PresentationLayer, "MVC", StringComparison.OrdinalIgnoreCase)) ?? uiHintAttributes.FirstOrDefault(a => String.IsNullOrEmpty(a.PresentationLayer)); if (uiHintAttribute != null) { metadata.AdditionalValues.Add("TemplateControlParameters", uiHintAttribute.ControlParameters); } return metadata; }Then in my editor template I did this:
string type = ""; if (ViewContext.ViewData.ModelMetadata.AdditionalValues.ContainsKey("TemplateControlParameters")) { //Get the type of Bulk Files Dictionary<string, object> d = (Dictionary<string, object>)ViewContext.ViewData.ModelMetadata.AdditionalValues["TemplateControlParameters"]; if (d.ContainsKey("Type")) { type = (string)d["Type"]; } }My code was created when MVC2 was the latest. While it still works, MVC3 does make things quite a bit simpler. Nicely done!