List Fields
If you want the user to specify an item from an arbitrary list of objects you can use the [ExistsIn]
attribute against a model property. The property just needs to be the same type as the property of the list element type that represents the "value" of the object, e.g.:
public class MyObject
{
public string Name { get; set; }
public int Id { get; set; } // This is the "value" of the object
}
public class MyViewModel
{
public MyViewModel() {
ListValues = new List<MyObject>
{
new MyObject { Id = 1, Name = "First item" },
new MyObject { Id = 2, Name = "Second item" },
};
}
...
public List<MyObject> ListValues { get; set; }
[ExistsIn(nameof(ListValues), nameof(MyObject.Id), nameof(MyObject.Name))]
public int ListId { get; set; } // Same type as Id
[ExistsIn(nameof(ListValues), nameof(MyObject.Id), nameof(MyObject.Name))]
public int? NullableListId { get; set; } // You can specify the same type with the Nullable modifier to indicate it's optional
}
The ExistsIn
attribute looks like this:
/// <summary>
/// Indicates that the attributed property value should exist within the list property referenced by the attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class ExistsInAttribute : ValidationAttribute, IModelMetadataAware
{
/// <summary>
/// Application-wide configuration for whether or not to enable ExistsIn validation.
/// </summary>
public static bool EnableValidation = true;
/// <summary>
/// Instantiates an <see cref="ExistsInAttribute"/>.
/// </summary>
/// <param name="listProperty">The name of the property containing the list this property should reference.</param>
/// <param name="valueProperty">The name of the property of the list items to use for the value</param>
/// <param name="nameProperty">The name of the property of the list items to use for the name/label</param>
public ExistsInAttribute(string listProperty, string valueProperty, string nameProperty);
/// <summary>
/// Instantiates an <see cref="ExistsInAttribute"/>.
/// </summary>
/// <param name="listProperty">The name of the property containing the list this property should reference.</param>
/// <param name="valueProperty">The name of the property of the list items to use for the value</param>
/// <param name="nameProperty">The name of the property of the list items to use for the name/label</param>
/// <param name="enableValidation">Optional override for ExistsIn server-side validation configuration (if not specified, static configuration setting ExistsInAttribute.EnableValidation is used)</param>
public ExistsInAttribute(string listProperty, string valueProperty, string nameProperty, bool enableValidation);
}
Default HTML
Non-nullable list id (drop-down with no empty option)
When using the Default Field Generator then the default HTML of the Field Element for a non-nullable list id will be:
<select %validationAttrs% %htmlAttributes% id="%propertyName%" name="%propertyName%" required="required">
%foreach item x in Model.{ListProperty}%
<option value="%x.{ValueProperty}%">%x.{NameProperty}%</option>
%endforeach%
</select>
If the list id is non-nullable then the field will be Required regardless of whether you specified the [Required]
attribute.
So in the above example when outputting the Field Element HTML for the ListId
property it would have (assuming you didn't specify any additional HTML attributes):
<select data-val="true" data-val-number="The field List id must be a number." data-val-required="The List id field is required." id="ListId" name="ListId" required="required">
<option value="1">First item</option>
<option value="2">Second item</option>
</select>
Nullable list id (drop-down with empty option)
When using the Default Field Generator then the default HTML of the Field Element for a nullable list id will be:
<select %validationAttrs% %htmlAttributes% id="%propertyName%" name="%propertyName%">
<option selected="selected" value="">%noneDescription%</option>
%foreach item x in Model.{ListProperty}%
<option value="%x.{ValueProperty}%">%x.{NameProperty}%</option>
%endforeach%
</select>
So in the above example when outputting the Field Element HTML for the ListId
property it would have (assuming you didn't specify any additional HTML attributes):
<select data-val="true" data-val-number="The field List id must be a number." id="ListId" name="ListId">
<option selected="selected" value=""></option>
<option value="1">First item</option>
<option value="2">Second item</option>
</select>
Server-side validation
If you want to provide server-side validation protection of the value the user submitted then the [ExistsIn]
attribute will automatically take care of this for you by default.
If you don't want to perform server-side validation then you can either:
Turn off Exists In validation globally by setting the appropriate setting in your
Application_Start
function (or a method it calls) withinGlobal.asax.cs
:ExistsInAttribute.EnableValidation = false;
Turn off validation on a per-usage basis by setting
false
to theenableValidation
value when adding the attribute, e.g.:[ExistsIn(nameof(ListValues), nameof(MyObject.Id), nameof(MyObject.Name), enableValidation: false)] public int ListId { get; set; }
If you turn off validation globally, but want to enable it for a specific usage then you can pass true
to the enableValidation
attribute - any value specified for it will override the global default.
If you want to take advantage of the server-side validation then the list needs to be populated when the DefaultModelBinder
binds the property with the [ExistsIn]
attribute specified. If the list is null at that point and validation is enabled then an exception will be thrown. If you want to specify the list at the right time then you have two options:
- Define the list in the model constructor (like the above example)
- Create a custom model binder for your model type that creates the list first
- This allows you to populate the list using a database by dependency injecting your database access component into the model binder
- This also allows you to easily unit test the model binder
For example:
public class InvoiceSelectionViewModel
{
[ReadOnly(true)]
public IList<Invoice> Invoices { get; set; }
[ExistsIn(nameof(Invoices), nameof(Invoices.Id), nameof(Invoices.InvoiceNumber))]
public int InvoiceId { get; set; }
}
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc(options => {
...
options.ModelBinderProviders.Insert(0, new InvoiceSelectionViewModelBinderProvider());
});
}
}
public class InvoiceSelectionViewModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(InvoiceSelectionViewModel))
{
return new BinderTypeModelBinder(typeof(InvoiceSelectionViewModelBinder));
}
return null;
}
}
public class InvoiceSelectionViewModelBinder : ComplexTypeModelBinder
{
private readonly IQueryExecutor _queryExecutor;
public InvoiceSelectionViewModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, ILoggerFactory loggerFactory, IQueryExecutor queryExecutor)
: base(propertyBinders, loggerFactory)
{
_queryExecutor = queryExecutor;
}
protected override async Task BindProperty(ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == nameof(InvoiceSelectionViewModel.Invoices))
{
var invoices = await _queryExecutor.ExecuteAsync(new GetInvoicesQuery(bindingContext.HttpContext.User.Identity));
bindingContext.Result = ModelBindingResult.Success(invoices);
}
else
{
await base.BindProperty(bindingContext);
}
}
}
Configurability
Display as list of radio buttons
You can force a list field to display as a list of radio buttons rather than a drop-down using the AsRadioList
method on the Field Configuration, e.g.:
The AsRadioList
method is mapped to as="RadioList"
.
<field for="ListId" as="RadioList" />
<field for="NullableListId" as="RadioList" />
This will change the default HTML for the non-nullable list id field and the Required nullable list id field as shown above to:
<ul>
%foreach item x in Model.{ListProperty} with increment i%
<li><input %validationAttrs% %htmlAttributes% id="%propertyName%_%i%" name="%propertyName%" required="required" type="radio" value="%x.{ValueProperty}%"> <label for="%propertyName%_%i%">%x.{NameProperty}%</label></li>
%endforeach%
</ul>
And it will change the default HTML for the non-Required nullable list id field as shown above to:
<ul>
<li><input %validationAttrs% %htmlAttributes% id="%propertyName%_1" name="%propertyName%" type="radio" value=""> <label for="%propertyName%_1">%noneDescription%</label></li>
%foreach item x in Model.{ListProperty} with increment i%
<li><input %htmlAttributes% id="%propertyName%_%i+1%" name="%propertyName%" type="radio" value="%x.{ValueProperty}%"> <label for="%propertyName%_%i+1%">%x.{NameProperty}%</label></li>
%endforeach%
</ul>
Change the text description of none
When you display a nullable list id field as a drop-down or a non-Required nullable list id field as a list of radio buttons you can change the text that is used to display the none
value to the user. By default the text used is an empty string for the drop-down and None
for the radio button. To change the text simply use the WithNoneAs
method, e.g.:
The WithNoneAs
method is mapped to none-label="{label}"
.
<field for="NullableListId" none-label="No value" />
This will change the default HTML for the nullable list id field as shown above to:
<select %validationAttrs% %htmlAttributes% id="%propertyName%" name="%propertyName%">
<option selected="selected" value="">No value</option>
@* List item values as <options>... *@
</select>
Hide empty item
If you have a nullable list id field as a drop-down or a non-Required nullable list id field as a list of radio buttons then it will show the empty item and this item will be selected by default if the field value is null. If for some reason you want one of these fields, but you would also like to hide the empty item you can do so with the HideEmptyItem
method in the Field Configuration, e.g.:
The HideEmptyItem
method is mapped to hide-empty-item="true"
.
<field for="NullableListId" hide-empty-item="true">
This will change the default HTML for the nullable list id field as shown above to:
<select %validationAttrs% %htmlAttributes% id="%propertyName%" name="%propertyName%">
@* List item values as <options>... *@
</select>