问题
I'm trying to use Data Annotations to add validation to a List in my model that cannot be empty. I've tried several implementations of a custom attribute, including ones here and here.
My view:
<div class="form-group">
@* Model has a list of ints, LocationIDs *@
@Html.LabelFor(model => model.LocationIDs, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
<select class="select2 form-control" multiple id="LocationIDs" name="LocationIDs">
@* Adds every possible option to select box *@
@foreach (LocationModel loc in db.Locations)
{
<option value="@loc.ID">@loc.Name</option>
}
</select>
@Html.ValidationMessageFor(model => model.LocationIDs, "", new { @class = "text-danger" })
</div>
</div>
Model:
public class ClientModel
{
public int ID { get; set; }
[Required] // Does nothing
public List<int> LocationIDs { get; set; }
}
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "ID,LocationIDs")] ClientModel clientModel)
{
if (ModelState.IsValid)
{
db.Clients.Add(clientModel);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(clientModel);
}
One of the (functionally identical) attributes I've tried:
[AttributeUsage(AttributeTargets.Property)]
public sealed class CannotBeEmptyAttribute : RequiredAttribute
{
public override bool IsValid(object value)
{
var list = value as IEnumerable;
return list != null && list.GetEnumerator().MoveNext();
}
}
Currently, checking for a null or empty list passes validation, even if nothing is chosen. In this case, a list of length one, containing the first option, is bound.
I've confirmed that the controller is actually sent a List of length one. I'm not sure how to change this behavior, however. I still think this may be what was described in the below block quote.
I think my problem may be described in this answer's edit, but I'm not sure how to solve it.
Excerpt below:
You'll also have to be careful how you bind your list in your view. For example, if you bind a List to a view like this:
<input name="ListName[0]" type="text" />
<input name="ListName[1]" type="text" />
<input name="ListName[2]" type="text" />
<input name="ListName[3]" type="text" />
<input name="ListName[4]" type="text" />
The MVC model binder will always put 5 elements in your list, all String.Empty. If this is how your View works, your attribute would need to get a bit more complex, such as using Reflection to pull the generic type parameter and comparing each list element with default(T) or something.
回答1:
You can try contructing the validator with a Type and verifying if any of the itens on the list is different of the default value of your type. Changing the example you mentioned here:
public class CannotBeEmptyAttribute : ValidationAttribute
{
private const string defaultError = "'{0}' must have at least one element.";
public Type ListType { get; private set; }
protected CannotBeEmptyAttribute(Type listType) : base(defaultError)
{
this.ListType = listType;
}
public override bool IsValid(object value)
{
object defaultValue = ListType.IsValueType ? Activator.CreateInstance(ListType) : null;
IEnumerable list = value as IEnumerable;
if (list != null)
{
foreach (var item in list)
{
if(item != defaultValue)
{
return true;
}
}
}
return false;
}
public override string FormatErrorMessage(string name)
{
return String.Format(this.ErrorMessageString, name);
}
}
回答2:
Your CannotBeEmptyAttribute is fine. I've used the exact same code and it works perfectly. Make sure you change your view model to use it as well (instead of the Required which doesn't do what you want it to in this case.
Your custom attribute won't provide you with client-side validation unless you implement it. This means that the form will get posted even though it's invalid but if (ModelState.IsValid) will catch it. Have you used a debugger to see if is IsValid is false?
"Manually" creating the input is completely fine for server-side validation but client-side won't work because the necessary data- attributes are missing.
Here's a minimal version of a form with manually created multi-select input that works with the CannotBeEmptyAttribute:
@using (Html.BeginForm("TestPost", "Home"))
{
<select multiple="multiple" name="TestList">
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
@Html.ValidationMessage("TestList")
@Html.ValidationSummary()
<input type="submit" value="Save"/>
}
回答3:
The only way I could get a zero-selection to actually bind as null was by using Html.ListBoxFor (which I couldn't figure out initially – it should have been done this way from the start):
@Html.ListBoxFor(model => model.SelectedLocations, Model.AllLocations, new { @class = "select2 form-control" })
I haven't gotten client-side validation to work, but I'll post that as another question.
来源:https://stackoverflow.com/questions/45201531/cant-force-listint-to-be-required