Strongly Typed ASP.Net MVC Helpers
I've put together a short tutorial on creating ASP.Net MVC Helper Controls that use lambda expressions to strongly type the model, property and value parameters.
Although the ASP.Net MVC helpers are really useful, the one major drawback for me is having to use magic strings to set property values, especially when the properties might be deeply nested lists, or even deeply nests lists and properties within deeply nested lists.
To fix this situation I have wrapped the ASP.Net MVC helpers in order to set properties and values using lambda expressions.
Example calls:
- <% using (this.Html.BeginForm("Update", "Home"))
- { %>
- <%= this.Html.RenderTextBoxFor(this.Model, c => c.FirstName)%>
- <%= this.Html.RenderValidationMessageFor(this.Model, c=>c.FirstName ) %>
- <%= this.Html.RenderTextBoxFor(this.Model, c => c.LastName)%>
- <%= this.Html.RenderValidationMessageFor(this.Model, c=>c.LastName ) %>
- <%if (this.Model != null)
- { %>
- <% for (int i = 0; i < this.Model.Addresses.Count; i++)
- { %>
- House number <%= this.Html.RenderTextBoxFor(this.Model, c => c.Addresses[i].HouseNumber)%>
- <%= this.Html.RenderValidationMessageFor(this.Model, c => c.Addresses[i].HouseNumber)%>
- Postcode <%= this.Html.RenderTextBoxFor(this.Model, c => c.Addresses[i].Postcode)%>
- <%= this.Html.RenderValidationMessageFor(this.Model, c => c.Addresses[i].Postcode)%>
- <%}%>
- <%} %>
- <input type="submit" value="Submit" />
- <%} %>
As you can see, the above example is a basic user model with a first name, last name and a list of addresses, each textbox also has a corresponding validation message for displaying any model state errors (mainly just to show that as the controls are wrapping the ASP.Net MVC Helpers, you can still utilise model state as normal).
The rendered html source has the correct names required for use with model binding.
For example:
- <form action="/Home/Update" method="post">
- <input id="FirstName" name="FirstName" type="text" value="Sean" />
- <input id="LastName" name="LastName" type="text" value="McAlinden" />
- House number <input id="Addresses[0]_HouseNumber" name="Addresses[0].HouseNumber" type="text" value="12" />
- Postcode <input id="Addresses[0]_Postcode" name="Addresses[0].Postcode" type="text" value="AB11 1CD" />
- House number <input id="Addresses[1]_HouseNumber" name="Addresses[1].HouseNumber" type="text" value="34" />
- Postcode <input id="Addresses[1]_Postcode" name="Addresses[1].Postcode" type="text" value="EF22 2GH" />
- <input type="submit" value="Submit" />
- </form>
In order to utilise this technique you need to create wrappers for each control you wish to use, below are examples of the two controls used above.
- using System;
- using System.Linq.Expressions;
- using System.Web.Mvc;
- using System.Web.Mvc.Html;
- namespace Demo.Helpers
- {
- /// <summary>
- /// Class with strong type wrappers for the mvc helper controls.
- /// </summary>
- public static class StronglyTypeControls
- {
- /// <summary>
- /// Renders an mvc textbox control.
- /// </summary>
- /// <typeparam name="TModel">Accepts the type of model.</typeparam>
- /// <typeparam name="TProperty">Accepts the type of property.</typeparam>
- /// <param name="html">Extends the html object.</param>
- /// <param name="model">Accepts the model.</param>
- /// <param name="property">Accepts a lambda expression representing the property.</param>
- /// <returns>Returns a string text box control.</returns>
- public static string RenderTextBoxFor<TModel, TProperty>
- (
- this HtmlHelper html,
- TModel model,
- Expression<Func<TModel, TProperty>> property
- ) where TModel : class
- {
- return RenderTextBoxFor(html, model, property, null);
- }
- /// <summary>
- /// Renders an mvc textbox control.
- /// </summary>
- /// <typeparam name="TModel">Accepts the type of model.</typeparam>
- /// <typeparam name="TProperty">Accepts the type of property.</typeparam>
- /// <param name="html">Extends the html object.</param>
- /// <param name="model">Accepts the model.</param>
- /// <param name="property">Accepts a lambda expression representing the property.</param>
- /// <param name="htmlAttributes">Accepts an object containing html attributes.</param>
- /// <returns>Returns a string text box control.</returns>
- public static string RenderTextBoxFor<TModel, TProperty>
- (
- this HtmlHelper html,
- TModel model,
- Expression<Func<TModel, TProperty>> property,
- object htmlAttributes
- ) where TModel : class
- {
- var propertyName = property.GetPropertyPath(model);
- var propertyValue = property.GetValue(model);
- var inputString = InputExtensions.TextBox(html, propertyName, propertyValue, htmlAttributes);
- return inputString;
- }
- /// <summary>
- /// Renders an mvc textbox control.
- /// </summary>
- /// <typeparam name="TModel">Accepts the type of model.</typeparam>
- /// <typeparam name="TProperty">Accepts the type of property.</typeparam>
- /// <param name="html">Extends the html object.</param>
- /// <param name="model">Accepts the model.</param>
- /// <param name="property">Accepts a lambda expression representing the property.</param>
- /// <returns>Retuns a rendered validation control.</returns>
- public static string RenderValidationMessageFor<TModel, TProperty>
- (
- this HtmlHelper html,
- TModel model,
- Expression<Func<TModel, TProperty>> property
- ) where TModel : class
- {
- var propertyName = property.GetPropertyPath(model);
- var validationString = ValidationExtensions.ValidationMessage(html, propertyName);
- return validationString;
- }
- }
- }
To finish this off, a helper class is required to generate the right property paths and values.
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.Linq.Expressions;
- using System.Reflection;
- using System.Text;
- namespace Demo.Helpers
- {
- /// <summary>
- /// Helper class to get property paths and values from a model.
- /// </summary>
- public static class StronglyTypedControlHelpers
- {
- /// <summary>
- /// Gets the value of a model property.
- /// </summary>
- /// <typeparam name="TModel">Accepts the type of model.</typeparam>
- /// <typeparam name="TProperty">Accepts the type of property.</typeparam>
- /// <param name="propertyExpression">Accepts a lambda expression representing the property to get the value on.</param>
- /// <param name="model">Accepts the model.</param>
- /// <returns>A string property value.</returns>
- public static string GetValue<TModel, TProperty>
- (
- this Expression<Func<TModel, TProperty>> propertyExpression,
- TModel model
- ) where TModel : class
- {
- if (model != default(TModel))
- {
- return propertyExpression.Compile().Invoke(model).ToString();
- }
- return string.Empty;
- }
- /// <summary>
- /// Gets the path of a property path including indexes on lists.
- /// </summary>
- /// <typeparam name="TModel">Accepts the type of model.</typeparam>
- /// <typeparam name="TProperty">Accepts the type of property.</typeparam>
- /// <param name="propertyExpression">Accepts a lambda expression representing the property to get the property path on.</param>
- /// <param name="model">Accepts the model.</param>
- /// <returns>A string property path.</returns>
- public static string GetPropertyPath<TModel, TProperty>
- (
- this Expression<Func<TModel, TProperty>> propertyExpression,
- TModel model
- ) where TModel : class
- {
- List<string> pathItems = new List<string>();
- BuildGraph(propertyExpression.Body, pathItems);
- return BuildProperty(pathItems);
- }
- private static string BuildProperty(List<string> pathItems)
- {
- StringBuilder sb = new StringBuilder();
- pathItems.Reverse();
- foreach (var item in pathItems)
- {
- if (sb.Length > 0)
- {
- sb.Append(".");
- }
- sb.Append(item);
- }
- return sb.ToString();
- }
- private static void BuildGraph(Expression expression, IList<string> pathItems)
- {
- if (expression is MemberExpression)
- {
- BuildMemberExpressionProperty(expression, pathItems);
- }
- if (expression is MethodCallExpression)
- {
- BuildMethodCallExpressionProperty(expression, pathItems);
- }
- }
- private static void BuildMethodCallExpressionProperty(Expression expression, IList<string> pathItems)
- {
- AddMethodCallExpressionPropertyName(expression, pathItems);
- if (MethodCallExpressionParent(expression) as ParameterExpression == null)
- {
- BuildGraph(MethodCallExpressionParent(expression), pathItems);
- }
- }
- private static Expression MethodCallExpressionParent(Expression expression)
- {
- return ((MemberExpression)(MemberExpression)((MethodCallExpression)expression).Object).Expression;
- }
- private static void BuildMemberExpressionProperty(Expression expression, IList<string> pathItems)
- {
- AddMemberExpressionPropertyName(expression, pathItems);
- if (MemberExpressionParent(expression) as ParameterExpression == null)
- {
- BuildGraph(MemberExpressionParent(expression), pathItems);
- }
- }
- private static Expression MemberExpressionParent(Expression expression)
- {
- return ((MemberExpression)expression).Expression;
- }
- private static void AddMethodCallExpressionPropertyName(Expression expression, IList<string> pathItems)
- {
- pathItems.Add(MethodCallExpressionPropertyWithIndex(expression));
- }
- private static string MethodCallExpressionPropertyWithIndex(Expression expression)
- {
- return MethodCallExpressionProperty(expression) + GetIndex((MethodCallExpression)expression);
- }
- private static string MethodCallExpressionProperty(Expression expression)
- {
- return ((MemberExpression)((MethodCallExpression)expression).Object).Member.Name;
- }
- private static void AddMemberExpressionPropertyName(Expression expression, IList<string> pathItems)
- {
- pathItems.Add(((MemberExpression)expression).Member.Name);
- }
- private static string GetIndex(MethodCallExpression method)
- {
- MemberExpression args = (MemberExpression)method.Arguments[0];
- object argValue = ((ConstantExpression)args.Expression).Value;
- FieldInfo field = args.Member.DeclaringType.GetField(args.Member.Name);
- int value = (int)field.GetValue(argValue);
- return string.Format(CultureInfo.CurrentCulture, "[{0}]", value);
- }
- }
- }
The last point to add is this solution doesn't add the parameter prefix to the html so in your ActionResult methods you will need to set the prefix to an empty string for example:
public ActionResult Update([Bind(Prefix="")]Customer customer){
// Update Codereturn View();}
At this point you should have the two strongly typed helpers working nicely, even with complex domain graphs.
To finish them off, it would be worth having some Design By Contract checks on the methods to ensure valid parameters are passed in.
Although this is only a basic introduction to wrapping the controls, it is an especially useful technique when using the ASP.Net MVC controls on more complex models.
I hope this post is useful.
Kind Regards,
Sean McAlinden.