Добрый день.
В этой статье я опишу как можно модифицировать автоматическую генерацию форм в ASP.NET MVC с помощью Data Annotation, в частности добавить поддержку Grid System.
Grid System (сетки) позволяет быстро создать вёрстку основанную на сетке. Данный результат достигается при помощи протестированных на кроссбраузерность пресетов ширины страницы, куда вы помещаете свой код.
Использование подобных систем позволяет структурировать форму, сэкономить пространство на странице, за счёт компоновки элементов. Кому-то может не понравиться ограничение, накладываем сетками, но они всё же позволяют быстро и эффективно создавать разметку на страницах.
Наиболее полярные grid system:
Для работы я выбрал последнюю систему, так как с помощью Twitter Bootstrap предоставляет разработчику множество готовых элементов, которые я так или иначе использую в разных проектах.
Выбираем Grid System
Для начала нужно выбрать подходящую нам grid system.
Для работы я выбрал последнюю систему. Эта система вялятся частью Twitter Bootstrap и предоставляет сетку шириной 940px и 12 колонок.
Пример реализации сетки:
<div class="row">
<div class="span4">...</div>
<div class="span8">...</div>
</div>
В контексте ASP.NET MVC:
<div class="row">
<div class="span4">
<div class="editor-label">
@Html.LabelFor(model => model.FirstName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.FirstName)
@Html.ValidationMessageFor(model => model.FirstName)
</div>
</div>
<div class="span4">
<div class="editor-label">
@Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.LastName)
@Html.ValidationMessageFor(model => model.LastName)
</div>
</div>
</div>
Не забываем подключить css к шаблону страницы:
<head>
...
<link href="@Url.Content("~/Content/bootstrap.css")" type="text/css" rel="stylesheet" />
...
</head>
Добавляем метаданные
Далее нам нужно добавить в модель метаданные (MetaData), на которые будем опираться в дальнейшем при построении разметки страницы. Для этого можно воспользоваться атрибутом [AdditionalMetadata].
Пример:
[UIHint("Currency")]
[Display(Name = "Розничная цена")]
[AdditionalMetadata("RowSpan", 4)]
public decimal RetailPrice{ get; set; }
Но при построении более сложной сетки такой вариант получается громоздким:
[UIHint("Currency")]
[Display(Name = "Розничная цена")]
[AdditionalMetadata("RowSpan", 4)]
[AdditionalMetadata("RowOffset", 1)]
[AdditionalMetadata("RowFluid", true)]
public decimal RetailPrice{ get; set; }
Поэтому создадим собственный элемент, вот пример его использования:
[UIHint("Currency")]
[Display(Name = "Розничная цена")]
[Row(4, Offset = 1, Fluid = true)]
public decimal RetailPrice{ get; set; }
И его реализация:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RowAttribute : Attribute, IMetadataAware
{
public RowAttribute(int span)
{
Span = span;
OpenRow = true;
CloseRow = true;
Offset = null;
IsFluid = false;
}
public bool OpenRow { get; set; }
public bool CloseRow { get; set; }
public int? Span { get; set; }
public int? Offset { get; set; }
public bool IsFluid { get; set; }
public void OnMetadataCreated(ModelMetadata metaData)
{
if (metaData == null) throw new ArgumentNullException();
if (IsFluid) metaData.AdditionalValues.Add("RowFluid", true);
if (!OpenRow) metaData.AdditionalValues.Add("RowOpenSkip", true);
if (!CloseRow) metaData.AdditionalValues.Add("RowCloseSkip", true);
if (Span.HasValue && Span.Value > 0) metaData.AdditionalValues.Add("RowSpan", Span.Value);
if (Offset.HasValue && Offset.Value > 0) metaData.AdditionalValues.Add("RowOffset", Offset.Value);
}
}
Атрибут должен изменять метаданные, поэтому необходимо добавить реализацию интерфейса IMetadataAware.
Метод OnMetadataCreated() вызывается при создании метаданных и получает их экземпляр в качестве параметра.
Описание ключей я приведу ниже.
Строим форму
Для автоматического создания формы воспользуемся методом @Html.EditorForModel(), возвращающим HTML-элемент ввода (input) для каждого свойства в модели.
Прмер реализации формы:
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Дверная коробка</legend>
@Html.EditorForModel()
<div class="form-controls">
<button type="submit">Сохранить</button>
<a href="@Url.Action("Index")">Вернуться</a>
</div>
</fieldset>
}
Теперь изменим шаблон, используемый при построении. Для этого создадим файл ViewsSharedEditorTemplatesObject.csthml.
Сразу приведу пример его реализации:
@if (ViewData.TemplateInfo.TemplateDepth > 5)
{
@(Model == null ? ViewData.ModelMetadata.NullDisplayText : ViewData.ModelMetadata.SimpleDisplayText)
}
else
{
foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay
&& !ViewData.TemplateInfo.Visited(pm)))
{
if (prop.HideSurroundingHtml)
{
@Html.Editor(prop.PropertyName)
}
else
{
if (!prop.AdditionalValues.ContainsKey("RowOpenSkip"))
{
@Html.Raw(String.Format("<div class="{0}">",
prop.AdditionalValues.ContainsKey("RowFluid") ?
"row-fluid" :
"row"))
}
if (prop.AdditionalValues.ContainsKey("RowSpan"))
{
var span = String.Format("span{0}", prop.AdditionalValues["RowSpan"]);
var offset = prop.AdditionalValues.ContainsKey("RowOffset") ?
String.Format("offset{0}", prop.AdditionalValues["RowOffset"]) :
String.Empty;
<div class="@span@offset">
<div class="display-label">
@Html.Label(prop.PropertyName)
</div>
<div class="display-field">
@Html.Editor(prop.PropertyName)
</div>
</div>
} else
{
<div class="display-label">
@Html.Label(prop.PropertyName)
</div>
<div class="display-field">
@Html.Editor(prop.PropertyName)
</div>
}
if (!prop.AdditionalValues.ContainsKey("RowCloseSkip"))
{
@Html.Raw("</div>")
}
}
}
}
Ключ RowOpenSkip указывает на то, что не нужно начинать новую строку. Аналогично ключ RowCloseSkip, указывает на то, что не нужно закрывать текущую строку.
Ключи Span и Offset задают используемые css классы span* и offset* соответственно.
Ключ RowFluid, указывает на то, что нам следует использовать Fluid grid system.
Результат
Теперь можем разметить модель в соответствии с потребностями.
Приведу лишь малый пример:
[UIHint("Currency")]
[Row(4, CloseRow = false)]
[Display(Name = "Розничная цена")]
public decimal RetailPrice{ get; set; }
[UIHint("Currency")]
[Row(4, OpenRow = false, CloseRow = false)]
[Display(Name = "Оптовая цена")]
public decimal WholesalePrice { get; set; }
[UIHint("Currency")]
[Row(4, OpenRow = false)]
[Display(Name = "Крупнооптовая цена")]
public decimal LargeWholesalePrice { get; set; }
Получаем:
<div class="row">
<div class="span4">
<div class="display-label">
Розничная цена
</div>
<div class="display-field">
<input type="text" value="0" name="RetailPrice" id="RetailPrice" ...>
</div>
</div>
<div class="span4">
<div class="display-label">
Оптовая цена
</div>
<div class="display-field">
<input type="text" value="0" name="WholesalePrice " id="WholesalePrice " ...>
</div>
</div>
<div class="span4">
<div class="display-label">
Крупнооптовая цена
</div>
<div class="display-field">
<input type="text" value="0" name="LargeWholesalePrice " id="LargeWholesalePrice " ...>
</div>
</div>
</div>
Заключение
В результате мы получили удобный способ компоновки форм с помощью Data Annotation. В дальнейшем вы можете подобным образом реализовать поддержку и других элементов из вашего любимого css фреймворка.
Некоторым может не понравиться сама идея описания представления в моделе, но предложенные способ позволяет достаточно быстро создать прототип разрабатываемой системы. К тому же вы можете заменить @Html.EditorForModel() на код нужной вам формы.
Стоит отметить, что использование подобного подхода оправданно в только для админ-зоны или back офиса, так как именно там необходимо строить большое количество типовых форм.
Удачи вам в ваших экспериментах!
Автор: VikentiyR