2

I have a model in which an employee contains a list of functions. The employee should at least have one function.

public class Employee
{
    [Required(ErrorMessage = "Name is Required")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Email is Required")]
    [RegularExpression(@"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}" +
                       @"\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\" +
                       @".)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$",
        ErrorMessage = "Email is not valid")]
    public string Email { get; set; }

    [Required(ErrorMessage = "At least one function is required")]
    public List<Function> Functions { get; set; }
}

public class Function
{
    [Required(ErrorMessage = "Name is Required")]
    public string Name { get; set; }
}

I've created an EditorTemplate for a Function

@model MvcClientSideValidation.Models.Function

    <fieldset>
        <legend>Functie</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

    </fieldset>

The Index view uses the EditorFor and a ValidationMessageFor.

@Html.EditorFor(m => m.Functions)
@Html.ValidationMessageFor(m => m.Functions)

The view also contains code to add or delete a function.

When the view is submitted, client-side validation does not check if a function is present. Server-side validation does. The problem is that when the list is empty, no input elements are rendered for the Function property, so there are no tags to which the validation tags can be added.

So I'm looking for an easy way to have unobtrusive client-side validation for a List with the [Required] attribute.

Edit: I just realized the [Required] attribute will probably only verify that Function is not null. It will not check if it contains any items. This is fine by me, as the property will become null automatically on postback.

comecme
  • 6,086
  • 10
  • 39
  • 67

3 Answers3

1

Try this:

[RequiredEnumerable(ErrorMessage = "At least one function is required")]
[UIHint("FunctionCollection")] // -> EditorTemplate partial view, it ensures that the input is generated.
public List<Function> Functions { get; set; }

RequiredEnumerable class:

public class RequiredEnumerableAttribute : ValidationAttribute, IClientValidatable
{
    public override bool IsValid(object value)
    {
        var enumerable = value as IEnumerable;
        if (enumerable == null) return false;

        IEnumerator enumerator = enumerable.GetEnumerator();
        if (enumerator.MoveNext())
            return true;
        return false;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        yield return new ModelClientValidationRule { ValidationType = "requiredenumerable", ErrorMessage = ErrorMessageString };
    }
}

FunctionCollection partial view (/Views/Shared/EditorTemplates/FunctionCollection.cshtml):

@model IEnumerable<Function>
@{
    Function[] models = Model != null ? Model as Function[] ?? Model.ToArray() : new Function[0];
    string value = Newtonsoft.Json.JsonConvert.SerializeObject(models.Select(x => x.Name));
}
@Html.Hidden(String.Empty, value)
@Html.ValidationMessage(String.Empty)
@if (models.Any())
{
    <ul>
        @foreach (Function f in models)
        {
            <li>@f.Name</li>
        }
    </ul>
}
@Ajax.ActionLink("Edit functions", "Index", "Function", new { selectedFunctions = value }, new AjaxOptions { })

in JavaScript:

<script>
    // create and append method 'requiredenumerable' to $.validator (before $.ready)
    $.validator.addMethod("requiredenumerable", function (value) {
        value = $.trim(value);
        if (!value.length) return false;

        //assumes that value is either a json array or a string of comma separated values​​
        var arr = value.indexOf("[") === 0 ? $.parseJSON(value) : value.split(",");
        return arr && arr.length > 0;
    });
    $.validator.unobtrusive.adapters.addBool("requiredenumerable");

    // as #Functions input is hidden so we need adapt the validator object (inside $.ready)
    $(document).ready(function () {
        var elemSelector = "#Functions";
        var validator = $(elemSelector).closest("form").data("validator");
        if (validator) {
            validator.settings.ignore = ":hidden:not(" + elemSelector + ")";
        }
    });
</script>
Charlie
  • 1,265
  • 15
  • 18
  • What's the HTML that's being generated? Because according to [Brad Wilson](http://forums.asp.net/post/4352873.aspx) unobtrusive validation using EditorTemplates this way is not supported as-is... – janv8000 Jul 04 '14 at 09:08
  • Could you show the contents of the FuctionCollection editor template? – janv8000 Jul 04 '14 at 09:23
  • @janv8000, I modified my answer to make it clearer. I am currently using it and it works fine. – Charlie Jul 05 '14 at 13:10
1

It's been a few years and this post comes up for searches related to .NET Core, so I hope it's okay that I post an answer related to .NET Core... The idea should be the same in the stack in question.

We have several validations like this as well. Without much custom code you can add a property to your model like so:

public class Employee
{
    /* other properties */

    public List<Function> Functions { get; } = new List<Function>();

    // add a "Count Validation" property against any required lists
    [Range(1, int.MaxValue)]
    public int FunctionsCount => Functions.Count; 
}

HTML:

@model Employee
<ul>
        @for (var i = 0; i < Model.Functions.Count; i++)
        {
            <li>
                <input type="hidden" asp-for="Functions[i].Name" />
                @f.Name
           </li>
        }
</ul>

@* Add the count input and validator *@
<input type="hidden" asp-for="FunctionsCount" />
<span asp-validation-for="FunctionsCount"></span>

Now unobtrusive-validation kicks in automatically to validate that FunctionsCount is greater or equal to 1. The only JS you will need is to set the count by hand whenever a function is added or removed:

$("#FunctionsCount").val($("li").length); // whatever selector gets you the number of functions. I used li since in my example the number of li elements is equivalent to the number of functions the user setup.
William Herrmann
  • 343
  • 2
  • 10
0

I think your problem is that you're allowing the list to be empty. If it truly requires a function to be present, then start with 1 item in the list, with nothing selected in the fields. Do not allow your code that adds or removes items to remove the last item, thus there will always be at least one and validation should work.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • Maybe I oversimplified my question. In the real solution the model of a list of uploaded files, and at least one file should be uploaded. I'm really looking for a solution to have the same validation that happens server-side also fire client-side. – comecme Feb 17 '14 at 09:37
  • @comecme - Yes, what I outlined would happen client-side. I don't understand your clarification. You simply always have at least textbox 1 (empty or not) at all times. I assume you have some kind of add item functionality to increase the number of files you upload, and a remove item functionality to delete them from the list. Simply don't allow the last item to be removed, so you always have at least one set of form fields to validate. – Erik Funkenbusch Feb 17 '14 at 09:40
  • That would mean I have to make sure I don't render anything if the filename is empty. Also, upon uploading a file, if the list only contains an empty filename, I should use that entry. For the second file I a new entry has to be added to the list. Not as simple as I would have liked. – comecme Feb 17 '14 at 10:35