1: <div class="editor-label">
2: <%=Html.LabelFor(m => m.LastName)% 3: </div> 1: [Required]2: [DisplayName("Last Name")]
3: [StringLength(64)]4: public string LastName { get; set; }
The problem with Attributes is that there is no reasonable way I’ve seen to create a DisplayName attribute style variant that is resource-aware. This is primarily due to constraints on Attributes in the form of generic and runtime type constraints. In short, there is no way to create a flexible approach with (1) no magic strings involved and (2) a format convention that can vary across different MVC Areas.
After looking over the first approach, I looked at creating a new LabelFor extension method. I had two driving criteria in creating the solution - (1) avoid any magic strings in the views and (2) allow for easy and flexible variation of naming rules associated with resource classes.
I like the result as the core workings are very testable and the flexibility is straight-forward.
The first part of the solution includes the LabelFor extension. You’ll notice it mirrors the LabelFor(Expression<…>) implementation by adding the ResourcePropertyResolver<T> implementation. In order to grab the associated metadata, I am relying on the same ModelMetadata calls that the existing LabelFor implementation uses. It then relies on the existing Label(string) implementation to generate the output.
1: /// <summary>
2: /// Our own set of custom label extensions
3: /// </summary>
4: public static class CustomLabelExtensions
5: {6: /// <summary>
7: /// Returns an HMTL label element with the content resolved for the given propety according to
8: /// the format specified in the parameters
9: /// </summary>
10: /// <typeparam name="TModel">The model typically inferred from the prage</typeparam>
11: /// <typeparam name="TValue">Value type inferred from expression</typeparam>
12: /// <typeparam name="TResourceType">Resource type used as the string resolution target</typeparam>
13: /// <param name="html">extension method parameter</param>
14: /// <param name="resourcePropertyResolver">resource resolver that indicates the resolution target
15: /// and format strings</param>
16: /// <param name="expression">expression tree intended to point at the property being named</param>
17: /// <returns>localized, resolved label for the property</returns>
18: public static MvcHtmlString LabelFor<TModel, TValue, TResourceType>(this HtmlHelper<TModel> html, ResourcePropertyResolver<TResourceType> resourcePropertyResolver, Expression<Func<TModel, TValue>> expression)
19: { 20: var metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData); 21: 22: var resourcePropertyName = string.Format(CultureInfo.InvariantCulture, resourcePropertyResolver.ResourcePropertyFormatPolicy,
23: metaData.ContainerType.Name, metaData.PropertyName); 24: 25: return html.Label(resourcePropertyResolver.GetResourceValue(resourcePropertyName));
26: } 27: }The next part is a default implementation of ResourcePropertyResolver. Starting at the top, this is a generic class requires the type of your resource class. A resource class is effectively any class that has static properties – the code-gen component of Visual Studio takes the resource table and implements it as a plain old class. Remember that the code generator property defaults to internal types – called out on the Custom Tool property. You can make these all public types by changing the Custom Tool property to PublicResXFileCodeGenerator.
First off, a static readonly type is resolved once for the class to avoid multipel typeof(…) resolutions – an efficiency move here. Next, a couple of string formatting properties. The first calls out the naming policy for properties on the resource class. The default value is ‘[class name]_[property name]’. The second format calls out the value that should be returned when a matching value is not found. Note that both of these are overridable which I will demonstrate in a moment.
Finally, the GetResourceValue method does the heavy lifting of locating the property in the table and formatting output accordingly.
1: /// <summary>
2: /// Implements access of the actual resource class and the associated
3: /// formatting rules for property names and unresolved resource values
4: /// </summary>
5: /// <typeparam name="TResourceType">resource class</typeparam>
6: public class ResourcePropertyResolver<TResourceType>
7: {8: private static readonly Type _resourceType = typeof(TResourceType);
9: 10: public virtual string ResourcePropertyFormatPolicy
11: { get { return "{0}_{1}"; } }
12: 13: public virtual string UnresolvedValueFormatString
14: { get { return "{0}.{1}"; } }
15: 16: internal string GetResourceValue(string resourcePropertyName)
17: {18: // both public and non-public are members are being searched because resource properties
19: // will either be generated as internal or public. Optimize this by making it specific to
20: // your case.
21: var propertyInfo = 22: _resourceType.GetProperty(resourcePropertyName, 23: BindingFlags.Static BindingFlags.Public BindingFlags.NonPublic); 24: 25: if (propertyInfo == null)
26: return String.Format(CultureInfo.InvariantCulture, UnresolvedValueFormatString, _resourceType.Name, resourcePropertyName);
27: 28: return (string)propertyInfo.GetValue(null, null);
29: } 30: }Next, let’s create a custom rule that outputs a patterned, more identifiable output with a little helper that simplifies the View markup. In this case, we simply override the rules we want to and then create a single live, static instance that is quickly referencable.
1: public class WebStringResolver : ResourcePropertyResolver<WebStringTable>
2: {3: public override string UnresolvedValueFormatString
4: { 5: get 6: {7: return "*** {0}.{1} ***";
8: } 9: } 10: 11: private static WebStringResolver _instance = new WebStringResolver();
12: public static WebStringResolver Instance
13: { 14: get 15: {16: return _instance;
17: } 18: } 19: }So finally, this leads us to the modified markup that is quite explicit to which resource table it is tied and no magic strings in place.
1: <div class="editor-label">
2: <%=Html.LabelFor(WebStringResolver.Instance, m => m.LastName)%> 3: </div>So there you have it- my diversion from reflector-spellunking into the ModelBinder.
No comments:
Post a Comment