DataAnnotations "NotRequired" attribute
Asked Answered
P

4

16

I've a model kind of complicated.

I have my UserViewModel which has several properties and two of them are HomePhone and WorkPhone. Both of type PhoneViewModel. In PhoneViewModel I have CountryCode, AreaCode and Number all strings. I want to make the CountryCode optional but AreaCode and Number mandatory.

This works great. My problem is that in the UserViewModel WorkPhone is mandatory, and HomePhone is not.

Is there anyway I can dissable Require attributs in PhoneViewModel by setting any attributes in HomeWork property?

I've tried this:

[ValidateInput(false)]

but it is only for classes and methods.

Code:

public class UserViewModel
{
    [Required]
    public string Name { get; set; }

    public PhoneViewModel HomePhone { get; set; }

    [Required]    
    public PhoneViewModel WorkPhone { get; set; }
}

public class PhoneViewModel
{
    public string CountryCode { get; set; }

    public string AreaCode { get; set; }

    [Required]
    public string Number { get; set; }
}
Puerile answered 23/5, 2012 at 14:49 Comment(4)
Can you show the code of your models/viewmodels please?Ryals
Yes, sure. Give a few minutes because I wrote an example, that isn't EXACTLY my case.Puerile
Ok, I may not answer for the next hour so due to a meeting so hopefully someone else can!Ryals
For reference, the ValidateInput attribute isn't named the best - it's actually more a security filter, not model validation. It validates the safety, not the 'correctness.' From MSDN: "[ValidateInput] works by checking all input data against a hard-coded list of potentially dangerous data."Jaquelinejaquelyn
D
6

[UPDATED on 5/24/2012 to make the idea more clear]

I'm not sure this is the right approach but I think you can extend the concept and can create a more generic / reusable approach.

In ASP.NET MVC the validation happens at the binding stage. When you are posting a form to the server the DefaultModelBinder is the one that creates model instances from the request information and add the validation errors to the ModelStateDictionary.

In your case, as long as the binding happens with the HomePhone the validations will fire up and I think we can't do much about this by creating custom validation attributes or similar kind.

All I'm thinking is not to create model instance at all for HomePhone property when there are no values available in the form (the areacode, countrycode and number or empty), when we control the binding we control the validation, for that, we have to create a custom model binder.

In the custom model binder we are checking if the property is HomePhone and if the form contains any values for it's properties and if not we don't bind the property and the validations won't happen for HomePhone. Simply, the value of HomePhone will be null in the UserViewModel.

  public class CustomModelBinder : DefaultModelBinder
  {
      protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
      {
        if (propertyDescriptor.Name == "HomePhone")
        {
          var form = controllerContext.HttpContext.Request.Form;

          var countryCode = form["HomePhone.CountryCode"];
          var areaCode = form["HomePhone.AreaCode"];
          var number = form["HomePhone.Number"];

          if (string.IsNullOrEmpty(countryCode) && string.IsNullOrEmpty(areaCode) && string.IsNullOrEmpty(number))
            return;
        }

        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
      }
  }

Finally you have to register the custom model binder in global.asax.cs.

  ModelBinders.Binders.Add(typeof(UserViewModel), new CustomModelBinder());

So now of you have an action that takes UserViewModel as parameter,

 [HttpPost]
 public Action Post(UserViewModel userViewModel)
 {

 }

Our custom model binder come into play and of form doesn't post any values for the areacode, countrycode and number for HomePhone, there won't be any validation errors and the userViewModel.HomePhone is null. If the form posts atleast any one of the value for those properties then the validation will happen for HomePhone as expected.

Datum answered 23/5, 2012 at 17:28 Comment(1)
I've just left home. What does this exactly do? I'll try it tomorrow.Puerile
S
3

I've been using this amazing nuget that does dynamic annotations: ExpressiveAnnotations

It allows you to do things that weren't possible before such as

[AssertThat("ReturnDate >= Today()")]
public DateTime? ReturnDate { get; set; }

or even

public bool GoAbroad { get; set; }
[RequiredIf("GoAbroad == true")]
public string PassportNumber { get; set; }

Update: Compile annotations in a unit test to ensure no errors exist

As mentioned by @diego this might be intimidating to write code in a string, but the following is what I use to Unit Test all validations looking for compilation errors.

namespace UnitTest
{
    public static class ExpressiveAnnotationTestHelpers
    {
        public static IEnumerable<ExpressiveAttribute> CompileExpressiveAttributes(this Type type)
        {
            var properties = type.GetProperties()
                .Where(p => Attribute.IsDefined(p, typeof(ExpressiveAttribute)));
            var attributes = new List<ExpressiveAttribute>();
            foreach (var prop in properties)
            {
                var attribs = prop.GetCustomAttributes<ExpressiveAttribute>().ToList();
                attribs.ForEach(x => x.Compile(prop.DeclaringType));
                attributes.AddRange(attribs);
            }
            return attributes;
        }
    }
    [TestClass]
    public class ExpressiveAnnotationTests
    {
        [TestMethod]
        public void CompileAnnotationsTest()
        {
            // ... or for all assemblies within current domain:
            var compiled = Assembly.Load("NamespaceOfEntitiesWithExpressiveAnnotations").GetTypes()
                .SelectMany(t => t.CompileExpressiveAttributes()).ToList();

            Console.WriteLine($"Total entities using Expressive Annotations: {compiled.Count}");

            foreach (var compileItem in compiled)
            {
                Console.WriteLine($"Expression: {compileItem.Expression}");
            }

            Assert.IsTrue(compiled.Count > 0);
        }


    }
}
Sarsaparilla answered 20/4, 2016 at 12:43 Comment(3)
Seems very flexible, don't really love to write code in a string thoughPuerile
Sure, same here, that's why at solution startup, you can register to compile all those attributes to detect any failures. More here: github.com/jwaliszko/…Sarsaparilla
-1. How is this any better than creating a validation attribute? The downsides to this is that there's another dependency, magic strings, and more pointless code. Not reusable as you'd be copy and pasting the attribute along with the magic string.Cortes
N
2

I wouldn't go with the modelBinder; I'd use a custom ValidationAttribute:

public class UserViewModel
{
    [Required]
    public string Name { get; set; }

    public HomePhoneViewModel HomePhone { get; set; }

    public WorkPhoneViewModel WorkPhone { get; set; }
}

public class HomePhoneViewModel : PhoneViewModel 
{
}

public class WorkPhoneViewModel : PhoneViewModel 
{
}

public class PhoneViewModel 
{
    public string CountryCode { get; set; }

    public string AreaCode { get; set; }

    [CustomRequiredPhone]
    public string Number { get; set; }
}

And then:

[AttributeUsage(AttributeTargets.Property]
public class CustomRequiredPhone : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ValidationResult validationResult = null;

        // Check if Model is WorkphoneViewModel, if so, activate validation
        if (validationContext.ObjectInstance.GetType() == typeof(WorkPhoneViewModel)
         && string.IsNullOrWhiteSpace((string)value) == true)
        {
            this.ErrorMessage = "Phone is required";
            validationResult = new ValidationResult(this.ErrorMessage);
        }
        else
        {
            validationResult = ValidationResult.Success;
        }

        return validationResult;
    }
}

If it is not clear, I'll provide an explanation but I think it's pretty self-explanatory.

Nunciata answered 24/5, 2012 at 9:17 Comment(3)
I think it is much simpler and elegant the other way. Also, it is better for maintenancePuerile
Why is it better for maintenance?? You don't have to register anything in global.asay, the ModelBinder-Solution works with hard coded strings, once you change your model property names, everything breaks... If you didn't get my workaround, I'll explain it with words.Nunciata
I did get your workarround. I edited the asnwer with the "generic" solution for the problem. You must wait the review to see it. It isn't with hardcoded strings anymore and the registration in global asax (as the creation of the class) is just once. In your workarround I must do all the work each time I have a new "child ViewModel" or, a different use of the child ViewModel (i.e. PhoneViewModel Cell)Puerile
I
1

Just some observation: the following code couse a problem if the binding is more than simple filed. I you have a case that in object have nested object it going to skip it and caouse that some filed not been binded in nested object.

Possible solution is

protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
     {
         if (!propertyDescriptor.Attributes.OfType<RequiredAttribute>().Any())
         {
             var form = controllerContext.HttpContext.Request.Form;

             if (form.AllKeys.Where(k => k.StartsWith(string.Format(propertyDescriptor.Name, "."))).Count() > 0)
             {
                 if (form.AllKeys.Where(k => k.StartsWith(string.Format(propertyDescriptor.Name, "."))).All(
                         k => string.IsNullOrWhiteSpace(form[k])))
                     return;
             }
         }

         base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
     }

much thanks to Altaf Khatri

Inductance answered 26/8, 2013 at 20:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.