23

I am using ASP.NET MVC 2 Preview 2 and have written a custom HtmlHelper extension method to create a label using an expression. The TModel is from a simple class with properties and the properties may have attributes to define validation requirements. I am trying to find out if a certain attribute exists on the property the expression represents in my label method.

The code for the class and label is:

public class MyViewModel
{
    [Required]
    public string MyProperty { get; set; }
}

public static MvcHtmlString Label<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string label)
{
    return MvcHtmlString.Create(string.Concat("<label for=\"", expression.GetInputName(), "\">", label, "</label>"));
}

public static string GetInputName<TModel, TProperty>(this Expression<Func<TModel, TProperty>> expression)
{
    return expression.Body.ToString().Substring(expression.Parameters[0].Name.Length + 1);
}

Then I would call the label like this:

Html.Label(x => x.MyProperty, "My Label")

Is there a way to find out if the property in the expression value passed to the Label method has the Required attribute?

I figured out that doing the following does get me the attribute if it exists, but I am hopeful there is a cleaner way to accomplish this.

public static MvcHtmlString Label<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string label)
{
    System.Attribute.GetCustomAttribute(Expression.Property(Expression.Parameter(expression.Parameters[0].Type, expression.GetInputName()), expression.GetInputName()).Member, typeof(RequiredAttribute))

    return MvcHtmlString.Create(string.Concat("<label for=\"", expression.GetInputName(), "\">", label, "</label>"));
}
Bernd
  • 330
  • 1
  • 2
  • 7

2 Answers2

53

Your expression parsing logic could use some work. Rather than deal with the actual types, you are converting to strings.

Here is a set of extension methods that you might use instead. The first gets the name of the member. The second/third combine to check if the attribute is on the member. GetAttribute will return the requested attribute or null, and the IsRequired just checks for that specific attribute.

public static class ExpressionHelpers
{
    public static string MemberName<T, V>(this Expression<Func<T, V>> expression)
    {
        var memberExpression = expression.Body as MemberExpression;
        if (memberExpression == null)
            throw new InvalidOperationException("Expression must be a member expression");

        return memberExpression.Member.Name;
    }

    public static T GetAttribute<T>(this ICustomAttributeProvider provider) 
        where T : Attribute
    {
        var attributes = provider.GetCustomAttributes(typeof(T), true);
        return attributes.Length > 0 ? attributes[0] as T : null;
    }

    public static bool IsRequired<T, V>(this Expression<Func<T, V>> expression)
    {
        var memberExpression = expression.Body as MemberExpression;
        if (memberExpression == null)
            throw new InvalidOperationException("Expression must be a member expression");

        return memberExpression.Member.GetAttribute<RequiredAttribute>() != null;
    }
}

Hopefully this helps you out.

Chris Patterson
  • 28,659
  • 3
  • 47
  • 59
  • This is much better, thanks! Is it possible to change GetAttribute to be an extension method of the Expression? That would allow an easy check on any expression for an attribute. – Bernd Oct 15 '09 at 11:55
  • +1 Great code man! I will mention this in my book "ASP.NET MVC Cookbook" (http://groups.google.com/group/aspnet-mvc-2-cookbook-review) – Andrew Siemer May 17 '10 at 03:36
  • 1
    So I used this solution for a long time, but recently revisited it when working with EntityFramework's `DbSet.Include`, which fails to properly eager-load nested properties (i.e. `Thing1.Thing2` of `o => o.Thing1.Thing2`). There's a [slightly more robust version](http://stackoverflow.com/a/2916344/1037948) of yours which takes into account `UnaryExpression`, but the string conversion you suggested avoiding [seems to be the easiest way](http://stackoverflow.com/a/17220748/1037948) to get the "fully-qualified" name. – drzaus Jun 20 '13 at 18:33
6

How about this code (from the MVC project on codeplex)

public static bool IsRequired<T, V>(this Expression<Func<T, V>> expression, HtmlHelper<T> htmlHelper)
    {
        var modelMetadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        string modelName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
        FieldValidationMetadata fieldMetadata = ApplyFieldValidationMetadata(htmlHelper, modelMetadata, modelName);
        foreach (var item in fieldMetadata.ValidationRules)
        {
            if (item.ValidationType == "required")
                return true;
        }

        return false;
    }

    private static FieldValidationMetadata ApplyFieldValidationMetadata(HtmlHelper htmlHelper, ModelMetadata modelMetadata, string modelName)
    {
        FormContext formContext = htmlHelper.ViewContext.FormContext;
        FieldValidationMetadata fieldMetadata = formContext.GetValidationMetadataForField(modelName, true /* createIfNotFound */);

        // write rules to context object
        IEnumerable<ModelValidator> validators = ModelValidatorProviders.Providers.GetValidators(modelMetadata, htmlHelper.ViewContext);
        foreach (ModelClientValidationRule rule in validators.SelectMany(v => v.GetClientValidationRules()))
        {
            fieldMetadata.ValidationRules.Add(rule);
        }

        return fieldMetadata;
    }
Ronnie
  • 61
  • 1
  • 1
  • I don't really understand this code, but I cut and pasted it in my HtmlHelperExtensionMethods and it worked as is. :) The other solution did not work for me because I use MetadataType. – RitchieD Oct 31 '14 at 13:40