ViewModel validation for a List
Asked Answered
Q

7

90

I have the following viewmodel definition

public class AccessRequestViewModel
{
    public Request Request { get; private set; }
    public SelectList Buildings { get; private set; }
    public List<Person> Persons { get; private set; }
}

So in my application there must be at least 1 person for an access request. What approach might you use to validate? I don't want this validation to happen in my controller which would be simple to do. Is the only choice a custom validation attribute?

Edit: Currently performing this validation with FluentValidation (nice library!)

RuleFor(vm => vm.Persons)
                .Must((vm, person) => person.Count > 0)
                .WithMessage("At least one person is required");
Quartersaw answered 28/2, 2011 at 19:40 Comment(0)
P
190

If you are using Data Annotations to perform validation you might need a custom attribute:

public class EnsureOneElementAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        var list = value as IList;
        if (list != null)
        {
            return list.Count > 0;
        }
        return false;
    }
}

and then:

[EnsureOneElement(ErrorMessage = "At least a person is required")]
public List<Person> Persons { get; private set; }

or to make it more generic:

public class EnsureMinimumElementsAttribute : ValidationAttribute
{
    private readonly int _minElements;
    public EnsureMinimumElementsAttribute(int minElements)
    {
        _minElements = minElements;
    }

    public override bool IsValid(object value)
    {
        var list = value as IList;
        if (list != null)
        {
            return list.Count >= _minElements;
        }
        return false;
    }
}

and then:

[EnsureMinimumElements(1, ErrorMessage = "At least a person is required")]
public List<Person> Persons { get; private set; }

Personally I use FluentValidation.NET instead of Data Annotations to perform validation because I prefer the imperative validation logic instead of the declarative. I think it is more powerful. So my validation rule would simply look like this:

RuleFor(x => x.Persons)
    .Must(x => x.Count > 0)
    .WithMessage("At least a person is required");
Pineda answered 28/2, 2011 at 19:44 Comment(7)
It looks like I need to use the overload Must() to use persons.Count, please see my edit and let me know if you have a version that is friendlier :)Quartersaw
@ryan, indeed there are two overloads of this method as shown in the documentation. So my version is friendlier. Don't worry if Visual Studio underlines it as error. It should work if you try to compile. It's just that VS Intellisense is not advanced enough to understand it :-) So RuleFor(x => x.Persons).Must(x => x.Count > 0).WithMessage("At least a person is required"); will compile and work fine.Pineda
Strange, now it isn't underlining. Thanks!Quartersaw
Thank you Darin, you picked a cool attribute name (: Just curious if you are using any validation framework for client-side as well. Built-in data annotations provide the advantage of client side if one find useful.Teahouse
How should I use it in razor? If i write @foreach(var p in Model.Person){...} @Html.ValidationMessageFor(model => Model.Person) validation server method is called but validation message is not shown.Norwood
Darin, is there any way to plumb this up with IClientValidatable? It's hard because we're never rendering an input control for List<Person> Persons specifically.... it's EditorFor usually just contains a collection of the individual models, so the data-val-* attributes don't have an <input name="Persons" /> specifically to attach to and subsequently get parsed by Unobtrusive Validation.Pothouse
I would like normal validation to take place if one person is added to see if that person has a name populated. A normal [Required] on person.Name. The validation check does skip if List<Person> is null. Good start. But it's a real issue since model validation gets passed if a person is added without a name. I think the issue might be Blazor specificJaundice
S
25

Following code works in asp.net core 1.1.

[Required, MinLength(1, ErrorMessage = "At least one item required in work order")]
public ICollection<WorkOrderItem> Items { get; set; }
Shammy answered 31/8, 2017 at 19:37 Comment(3)
It seems that this no longer works in .NET Core 2.1.0 (Preview 1)Schlessel
This works in .Net Core 2.2, I have tried this on a List type. If you want a separate custom message for [Required] attribute that's possible too. [Required(ErrorMessage = "Missing Meters"), MinLength(1, ErrorMessage = "Atleast 1 meter is required")] public List<Meter> Meters { get; set; }Czechoslovakia
Nowadays, MinLength DataAnnotation is working fine. I tested it in .Net 5. Also, I think Required it is not necessary, since MinLength already implies it. I let you MinLength official documentation learn.microsoft.com/en-us/dotnet/api/…Haggadist
A
19

Another possible way to handle the count validations for view model object's collection members, is to have a calculated property returning the collection or list count. A RangeAttribute can then be applied like in the code below to enforce count validation:

[Range(minimum: 1, maximum: Int32.MaxValue, ErrorMessage = "At least one item needs to be selected")]
public int ItemCount
{
    get
    {
        return Items != null ? Items.Length : 0;
    }
}

In the code above, ItemCount is an example calculated property on a view model being validated, and Items is an example member collection property whose count is being checked. In this example, at least one item is enforced on the collection member and the maximum limit is the maximum value an integer can take, which is, for most of the practical purposes, unbounded. The error message on validation failure can also be set through the RangeAttribute's ErrorMessage member in the example above.

Acrospire answered 14/8, 2015 at 14:33 Comment(0)
M
10

Darin's answer is good but the version below will automatically give you a useful error message.

public class MinimumElementsAttribute : ValidationAttribute
{
    private readonly int minElements;

    public MinimumElementsAttribute(int minElements)
    {
        this.minElements = minElements;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var list = value as IList;

        var result = list?.Count >= minElements;

        return result
            ? ValidationResult.Success
            : new ValidationResult($"{validationContext.DisplayName} requires at least {minElements} element" + (minElements > 1 ? "s" : string.Empty));
    }
}

Usage:

[MinimumElements(1)]
public List<Customer> Customers {get;set}

[MinimumElements(2)]
public List<Address> Addresses {get;set}

Error message:

  • Customers requires at least 1 element
  • Addresses requires at least 2 elements
Marillin answered 16/1, 2018 at 9:34 Comment(0)
D
2

You have two choices here, either create a Custom Validation Attribute and decorate the property with it, or you can make your ViewModel implement the IValidatableObject interface (which defines a Validate method)

Hope this helps :)

Dint answered 28/2, 2011 at 19:52 Comment(0)
H
0

One approach could be to use a private constructor and a static method to return an instance of the object.

public class AccessRequestViewModel
{
    private AccessRequesetViewModel() { };

    public static GetAccessRequestViewModel (List<Person> persons)
    {
            return new AccessRequestViewModel()
            {
                Persons = persons,
            };
    }

    public Request Request { get; private set; }
    public SelectList Buildings { get; private set; }
    public List<Person> Persons { get; private set; }
}

By always using the factory to instantiate your ViewModel, you can ensure that there will always be a person.

This probably isn't ideal for what you want, but it would likely work.

Harsho answered 28/2, 2011 at 19:45 Comment(0)
D
0

It would be very clean and elegant to have a custom validation. Something like this:

public class AccessRequestViewModel
{
    public Request Request { get; private set; }
    public SelectList Buildings { get; private set; }
    [AtLeastOneItem]
    public List<Person> Persons { get; private set; }
}

Or [MinimumItems(1)].

Doug answered 28/2, 2011 at 19:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.