16

I have a simple model:

public class Sample
{
    public bool A { get; set; }

    [Required]
    public bool B { get; set; }
}

A is obviously not required. Therefore, for validation have have set DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false in Global.asax.

I also have a simple html helper that prints true or false if the model is required:

public static class HtmlHelperExtensions
{
    public static MvcHtmlString IsRequired<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
    {
        var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);        
        return new MvcHtmlString(metadata.IsRequired.ToString());
    }
} 

I also wrote a view to showcase my issue:

@model MvcApplication10.Models.Sample

A: @Html.IsRequired(m => m.A), B: @Html.IsRequired(m => m.B)

I would have expected this to print A: false, B: true, however, it actually prints A: true, B: true.

Is there any way to make this print my expected result? IsRequired seems to always return true even though I have not explicitly set the RequiredAttribute. The docs state that it is true for non-nullable value types by default. How come there is no easy way to set this to false like we can with validation?

EDIT: I could write a custom provider like this, but I was wondering if there was an "easy" way around this:

public class ExtendedDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    private static bool addImplicitRequiredAttributeForValueTypes = false;

    public static bool AddImplicitRequiredAttributeForValueTypes
    {
        get
        {
            return addImplicitRequiredAttributeForValueTypes;
        }
        set
        {
            addImplicitRequiredAttributeForValueTypes = value;
        }
    }

    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var result = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);

        if (!AddImplicitRequiredAttributeForValueTypes && modelType.IsValueType && !attributes.OfType<RequiredAttribute>().Any())
        {
            result.IsRequired = false;
        }

        return result;
    }
}
TheCloudlessSky
  • 18,608
  • 15
  • 75
  • 116
  • 2
    In MVC5 simply setting `DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false` does now have the expected effect of not automatically adding `RequiredAttribute` for non-nullable value types. – Cocowalla Jan 19 '15 at 15:38

6 Answers6

11

As you noted, ValueTypes will default to true. To work around that, you can check for the RequiredAttribute if the type is a ValueType.

ModelMetadata metaData = ModelMetadata.FromLambdaExpression<TModel, TValue>(expression, html.ViewData);

if ((metaData.ModelType.IsValueType && metaData.ModelType.GetCustomAttributes(typeof(RequiredAttribute), false).Any()) ||
    (!metaData.ModelType.IsValueType && metaData.IsRequired))
{ ... }
Adam Spicer
  • 2,703
  • 25
  • 37
  • This is awesome. In case it helps anyone, I had to put together a label extension method that is placing the appropriate "*" html string in front of the field lable if the field is required. It is MVC5, and I was still having some issues with ValueTypes returning true. There are a lot of form fields (49), so I altered the above to use reflection to search the metadata.ContainerType to locate the desired property: metaData.ModelType.IsValueType && metadata.ContainerType.GetProperty(metadata.PropertyName).GetCustomAttributes(typeof(RequiredAttribute), false).Any(). – bphillips Dec 06 '15 at 01:54
4

If you are working in an EditorTemplate, as I was, you'll need one more step:

var metaData = ModelMetadata.FromLambdaExpression(Model => Model, ViewData);

var required = metaData.ContainerType.GetProperty(metaData.PropertyName).GetCustomAttributes(typeof (RequiredAttribute), false).Any();

You need to get the container type from the model metadata in order to check the attributes of your specific property; otherwise, you are just checking the attributes of the property's data type, not the attributes of the property.

SouthShoreAK
  • 4,176
  • 2
  • 26
  • 48
4

I guess you're facing a MVC bug. Required will always trigger on that situation, no matter what and even if you use

DataAnnotationsModelValidatorProvider
    .AddImplicitRequiredAttributeForValueTypes = false;

This was already discussed here and reported here. This example goes a step further and shows that when an implicit required triggers it doesn't prevent IValidatableObject from execute. If you run the demo from the second link you can reproduce your case, where required is always true.

Anyway, this is easy to solve because if you're saying that A is obviously not required is the same as saying that it is nullable, so just do it that way:

public bool? A { get; set; }
Community
  • 1
  • 1
Joao
  • 7,366
  • 4
  • 32
  • 48
  • Setting the property to `bool?` means I can't use built-in html helpers like `CheckBoxFor` which require a boolean. Yes - I could write my own html helper, but I'm still curious to see if writing a model metadata provider like I did in the OP makes more sense. – TheCloudlessSky Dec 02 '11 at 23:57
  • Being that your expected behavior, and considering that MVC currently behaves this way, I guess that a custom provider is your only option. – Joao Dec 03 '11 at 00:35
  • Is `A` property triggering validation? Because `IsRequired` being `true` doesn't mean the property isn't valid. – Joao Dec 03 '11 at 00:39
  • No it's not. I don't need this for validation, but rather for a custom HTML helper that is solely for display. – TheCloudlessSky Dec 03 '11 at 15:20
1

The following will return true or false depending on whether you have [Required] or not.

typeof(<YourModel>).GetProperty(<PropertyName>).GetCustomAttributes(typeof(RequiredAttribute), false).Any()

anIBMer
  • 1,159
  • 2
  • 12
  • 20
0

If you want it to work for nullable types too:

private static bool RequiredAttrExists(ModelMetadata metaData)
{
    if(!metaData.ModelType.IsValueType && metaData.IsRequired)
        return true;
    else if (metaData.ModelType.IsValueType && metaData.ContainerType.GetProperty(metaData.PropertyName).GetCustomAttributes(typeof(RequiredAttribute), false).Any())
        return true;
    return false;
}
yakya
  • 4,559
  • 2
  • 29
  • 31
0

I have had this problem in several places and have implemented a simple fix like this

if (metadata.ModelType == typeof(System.Boolean))
{
    metadata.IsRequired = false;
}
Eddie
  • 690
  • 10
  • 27