There's been a lot of discussion about this topic recently. Similar roadblocks are encountered with dates, date ranges and multi-select checkbox lists. Anywhere you might want to use a rich set of html controls. I've been experimenting with the concept of child ViewModels and I think the solution is cleaner than other approaches I've tried.
The basic concept is that you define a small view model that's closely coupled to a custom EditorTemplate.
In your example, we would start with a (child) ViewModel that's specific to a single select list:
public class SelectModel
{
#region SelectModel(string value, IEnumerable<SelectListItem> items)
public SelectModel(string value, IEnumerable<SelectListItem> items)
{
_value = value;
Items = new List<SelectListItem>(items);
_Select();
}
#endregion
// Properties
public List<SelectListItem> Items { get; private set; }
public string Value
{
get { return _value; }
set { _value = value; _Select();}
}
private string _value;
// Methods
private void _Select()
{
Items.ForEach(x => x.Selected = (Value != null && x.Value == Value));
}
}
In the view model that wants to use the dropdown you compose the select model (we're all using view models, right?):
public class EmailModel
{
// Constructors
public EmailModel()
{
Priority = new SelectModel("normal", _ToPrioritySelectItems());
}
// Properties
public SelectModel Priority { get; set; }
// Methods
private IEnumerable<SelectListItem> _ToPrioritySelectItems()
{
List<SelectListItem> result = new List<SelectListItem>();
result.Add(new SelectListItem() { Text = "High", Value = "high" });
...
}
Note this is a simple example with a fixed set of dropdown items. If they are coming from the domain layer, the controller passes them into the ViewModel.
Then add an editor template SelectModel.ascx in Shared/EditorTemplates
<%@ Control Inherits="System.Web.Mvc.ViewUserControl<SelectModel>" %>
<div class="set">
<%= Html.LabelFor(model => model) %>
<select id="<%= ViewData.ModelMetadata.PropertyName %>_Value" name="<%=ViewData.ModelMetadata.PropertyName %>.Value">
<% foreach (var item in Model.Items) { %>
<%= Html.OptionFor(item) %>
<% } %>
</select>
</div>
Note: OptionFor is a custom extension that does the obvious
The trick here is that the id and name are set using the compound format that the default ModelBinder expects. In our example "Priority.Value". So the string based Value property that is defined as part of SelectModel is set directly. The setter takes care of updating the list of Items to set the default select option if we need to redisplay the form.
Where this "child view model" approach really shines is more complex "control snippets of markup". I now have child view models that follow a similar approach for MultiSelect lists, Start/End date ranges, and Date + time combinations.
As soon as you go down this path, the next obvious question becomes validation.
I ended up having all of my child ViewModel's implement a standard interface:
public interface IValidatable
{
bool HasValue { get; }
bool IsValid { get; }
}
Then, I have a custom ValidationAttribute:
public class IsValidAttribute : ValidationAttribute
{
// Constructors
public IsValidAttribute()
{
ErrorMessage = "(not valid)";
}
// Properties
public bool IsRequired { get; set; }
// Methods
private bool Is(object value)
{
return value != null && !"".Equals(value);
}
public override bool IsValid(object value)
{
if (!Is(value) && !IsRequired)
return true;
if (!(value is IValidatable))
throw new InvalidOperationException("IsValidAttribute requires underlying property to implement IValidatable");
IValidatable validatable = value as IValidatable;
return validatable.IsValid;
}
}
Now you can just put attributes on properties that are child ViewModel based like any scalar property:
[IsValid(ErrorMessage = "Please enter a valid start date/time")]
public DateAndTimeModel Start { get; set; }