В последнее время было много дискуссий на эту тему. Подобные препятствия встречаются с датами, диапазонами дат и списками множественных флажков. Везде, где вы можете использовать богатый набор HTML-элементов управления. Я экспериментировал с концепцией дочерних ViewModels и думаю, что решение чище, чем другие подходы, которые я пробовал.
Основная концепция заключается в том, что вы определяете модель малого представления, которая тесно связана с пользовательским шаблоном EditorTemplate.
В вашем примере мы начнем с (дочерней) ViewModel, специфичной для одного списка выбора:
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));
}
}
В модели представления, которая хочет использовать раскрывающийся список, вы составляете выбранную модель (мы все используем модели представления, верно?):
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" });
...
}
Обратите внимание, что это простой пример с фиксированным набором выпадающих элементов. Если они поступают из уровня домена, контроллер передает их в ViewModel.
Затем добавьте шаблон редактора SelectModel.ascx в 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>
Примечание: OptionFor - это пользовательское расширение, которое делает очевидное
Хитрость в том, что идентификатор и имя задаются с использованием составного формата, который ожидает ModelBinder по умолчанию. В нашем примере «Приоритет. Значение». Таким образом, свойство Value на основе строки, определенное как часть SelectModel, устанавливается напрямую. Сеттер заботится об обновлении списка Предметов, чтобы установить опцию выбора по умолчанию, если нам нужно снова отобразить форму.
Там, где этот подход «модель дочернего представления» действительно сияет, более сложный «контрольный фрагмент разметки». Теперь у меня есть модели дочернего представления, которые следуют аналогичному подходу для списков MultiSelect, диапазонов дат начала / окончания и комбинаций Дата + время.
Как только вы идете по этому пути, следующий очевидный вопрос становится проверкой.
Я закончил тем, что все мои дочерние ViewModel реализовали стандартный интерфейс:
public interface IValidatable
{
bool HasValue { get; }
bool IsValid { get; }
}
Затем у меня есть собственный атрибут Validation:
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;
}
}
Теперь вы можете просто поместить атрибуты в свойства, основанные на дочернем ViewModel, как любое скалярное свойство:
[IsValid(ErrorMessage = "Please enter a valid start date/time")]
public DateAndTimeModel Start { get; set; }