I Just starting out w/ ASP.NET MVC 3 and I am trying to render out the following HTML for the string properties on a ViewModel on the create/edit view.
An much simpler solution is to implement a custom DataAnnotationsModelMetadataProvider
like this:
internal class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
ModelMetadata modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var maxLengthAttribute = attributes.OfType<MaxLengthAttribute>().SingleOrDefault();
if (maxLengthAttribute != null)
{
modelMetadata.AdditionalValues.Add("maxLength", maxLengthAttribute.Length);
}
return modelMetadata;
}
}
In the template you can simply use:
object maxLength;
ViewData.ModelMetadata.AdditionalValues.TryGetValue("maxLength", out maxLength);
@using System.ComponentModel.DataAnnotations
@model string
@{
var htmlAttributes = ViewData["htmlAttributes"] ?? new { @class = "checkbox-inline" };
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (!attributes.ContainsKey("maxlength"))
{
var metadata = ViewData.ModelMetadata;
var prop = metadata.ContainerType.GetProperty(metadata.PropertyName);
var attrs = prop.GetCustomAttributes(false);
var maxLength = attrs.OfType<MaxLengthAttribute>().FirstOrDefault();
if (maxLength != null)
{
attributes.Add("maxlength", maxLength.Length.ToString());
}
else
{
var stringLength = attrs.OfType<StringLengthAttribute>().FirstOrDefault();
if (stringLength != null)
{
attributes.Add("maxlength", stringLength.MaximumLength.ToString());
}
}
}
}
@Html.TextBoxFor(m => m, attributes)
To do this you'll need to create your own HtmlHelper extension and use reflection to get at the attributes on the model property. Look at the source code at http://codeplex.com/aspnet for the existing ...For()
HtmlHelper extensions. You'll need to get the PropertyInfo object for the model property using the expression that is passed in as the argument. They have several helper classes that should serve as templates for this. Once you have that, use the GetCustomAttributes method on the PropertyInfo to find the StringLength attribute and extract it's value. Since you'll be using a TagBuilder to create the input, add the length as an attribute via the TagBuilder.
...
var attribute = propInfo.GetCustomAttributes(typeof(StringLengthAttribute),false)
.OfType<StringLengthAttribute>()
.FirstOrDefault();
var length = attribute != null ? attribute.MaximumLength : 20; //provide a default
builder.Attributes.Add("maxlength",length);
...
return new MvcHtmlString( builder.ToString( TagRenderMode.SelfClosing ) );
}
See my comment on why I think this is a bad idea.
You can get the StringLength Validator from within an Editor Template, here are some examples:
https://jefferytay.wordpress.com/2011/12/20/asp-net-mvc-string-editor-template-which-handles-the-stringlength-attribute/
What I used and tested, as a result from the article above can be seen in my answer below (tested with MVC 5, EF 6) :
ASP.NET MVC 3 - Data Annoation and Max Length/Size for Textbox Rendering
Without being specific, I've personally had some mixed results with attempts to implement some other approaches, and I don't find either claimed method particular long; however, I did think some of the other approached looked a little "prettier".
A full code sample for tvanfosson's answer:
Model:
public class Product
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; }
EditorTemplates\String.cshtml
@model System.String
@{
var metadata = ViewData.ModelMetadata;
var prop = metadata.ContainerType.GetProperty(metadata.PropertyName);
var attrs = prop.GetCustomAttributes(false);
var maxLength = attrs.OfType<System.ComponentModel.DataAnnotations.MaxLengthAttribute>().FirstOrDefault();
}
<input id=@Html.IdForModel()@(metadata.IsRequired ? " required" : "")@(maxLength == null ? "" : " maxlength=" + maxLength.Length) />
HTML output:
<input id=Name maxlength=200 />
Ugly but it works. Now let's abstract it and clean it up a bit. Helper class:
public static class EditorTemplateHelper
{
public static PropertyInfo GetPropertyInfo(ViewDataDictionary viewData)
{
var metadata = viewData.ModelMetadata;
var prop = metadata.ContainerType.GetProperty(metadata.PropertyName);
return prop;
}
public static object[] GetAttributes(ViewDataDictionary viewData)
{
var prop = GetPropertyInfo(viewData);
var attrs = prop.GetCustomAttributes(false);
return attrs;
}
public static string GenerateAttributeHtml(ViewDataDictionary viewData, IEnumerable<Delegate> attributeTemplates)
{
var attributeMap = attributeTemplates.ToDictionary(t => t.Method.GetParameters()[0].ParameterType, t => t);
var attrs = GetAttributes(viewData);
var htmlAttrs = attrs.Where(a => attributeMap.ContainsKey(a.GetType()))
.Select(a => attributeMap[a.GetType()].DynamicInvoke(a));
string s = String.Join(" ", htmlAttrs);
return s;
}
}
Editor Template:
@model System.String
@using System.ComponentModel.DataAnnotations;
@using Brass9.Web.Mvc.EditorTemplateHelpers;
@{
var metadata = ViewData.ModelMetadata;
var attrs = EditorTemplateHelper.GenerateAttributes(ViewData, new Delegate[] {
new Func<StringLengthAttribute, string>(len => "maxlength=" + len.MaximumLength),
new Func<MaxLengthAttribute, string>(max => "maxlength=" + max.Length)
});
if (metadata.IsRequired)
{
attrs.Add("required");
}
string attrsHtml = String.Join(" ", attrs);
}
<input type=text id=@Html.IdForModel() @attrsHtml />
So you pass in an array of Delegates, and for each entry use a Func<AttributeTypeGoesHere, string>
, and then return whatever HTML string you wanted for each attribute.
This actually decouples well - you can map only the attributes you care about, you can map different sets for different parts of the same HTML, and the final usage (like @attrsHtml
) doesn't harm readability of the template.
Instead of the StringLength
attribute (because it's a validator attribute not a metadata provider) you can use the AdditionalMetadata
attribute. Sample usage:
public class ViewModel
{
[AdditionalMetadata("maxLength", 30)]
public string Property { get; set; }
}
Basically it puts the value 30 under the key maxLength in the ViewData.ModelMetadata.AdditionalValues
dictionary. So you can use it your EditorTemplate:
<input maxlength="@ViewData.ModelMetadata.AdditionalValues["maxLength"]" id="@ViewData.ModelMetadata.PropertyName" name="@ViewData.ModelMetadata.PropertyName" placeholder="@ViewData.ModelMetadata.DisplayName" type="text" value="@ViewData.Model" />