Freigeben über


Вспомогательные методы (хэлперы) в MVC Framework

В мире Web Forms можно легко многократно некоторую функциональность создав на ее основе элемент управления, за эту функциональность отвечающий. В MVC Framework эту функциональность несут методы расширения класса Html (называемые хэлперами). Разумеется, это лишь условность и на уровне представлений в MVC можно использовать свой собственный статический класс или экземпляр класса, передаваемый представлению через ViewData. Однако подобные условности упрощают жизнь будущим поколениям разработчиков, которые, возможно, будут поддерживать или повторно использовать ваш код. Поэтому использование стандартного подхода через расширение класса Html – хорошая идея.

По своей сущности хэлперы – обычные методы-расширения (extension methods, C# 3.0), принимающие произвольные набор параметров и возвращающие строковые значения. Например, создадим хэлпер, отображающий разметку для составного элемента управления на странице, позволяющего выбрать дату.

using System.Web.Mvc;

public static class DataPickerHelper
{
    public static string DatePicker(this HtmlHelper html, string id, string text)
    {
    }
}

Такой метод может быть использован в коде представления через синтаксиc <%= Html.DatePicker(“id”,“name”) %>.

Самое интересное, разумеется, это код самого хелпера. Давайте посмотрим на то, как можно подойти к созданию такого простого элемента как набор выпадающих списков для выбора даты.

Конкатенация строк

Самый очевидный способ – сгенерировать разметку конкатенацией строк.

using System.Web.Mvc;
using System.Text;
using System;
using System.Globalization;

public static class DataPickerHelper
{

    public static string DatePicker(this HtmlHelper html, string id)
    {
        return DatePicker(html, id, String.Empty);
    }

    public static string DatePicker(this HtmlHelper html, string id, string text)
    {

    StringBuilder sb = new StringBuilder();

    if (!String.IsNullOrEmpty(text))
    {

        sb.Append("<div id=\"");
        sb.Append(id);
        sb.Append("\">");
        sb.Append(text);
    }

    /* Day */
    sb.Append("<select id=\"day_");
    sb.Append(id);
    sb.Append("\">");
    sb.Append("<option></option>");

    for (int i = 1; i <= 31; i++)
    {
        sb.Append("<option>");
        sb.Append(i.ToString());
        sb.Append("</option>");
    }

    sb.Append("</select>");
    sb.Append("&nbsp;");

    /* Month */
    sb.Append("<select id=\"month_");
    sb.Append(id);
    sb.Append("\">");
    sb.Append("<option></option>");

    for (int i = 0; i <= 11; i++)
    {
        sb.Append("<option>");
        sb.Append(DateTimeFormatInfo.CurrentInfo.MonthNames[i]);
        sb.Append("</option>");
    }

    sb.Append("</select>");
    sb.Append("&nbsp;");

    /* Year */
    sb.Append("<select id=\"year_");
    sb.Append(id);
    sb.Append("\">");
    sb.Append("<option></option>");

    for (int i = 1900; i <= DateTime.Now.Year; i++)
    {
        sb.Append("<option>");
        sb.Append(i.ToString());
        sb.Append("</option>");
    }

    sb.Append("</select>");

    if (!String.IsNullOrEmpty(text))
    {
        sb.Append("</div>");
    }

    return sb.ToString();

    }

}

Плюс такого решения – оно очевидно и его можно создать «в лоб» скопировав разметку из макета верстки. Минусы тоже очевидны –поддержка сложна за счет того, что приходится работать с кодом, перемешанным с текстом.

Использование ресурсов

Упростить модификацию разметки и внесение косметических изменений можно за счет выделения статической разметки в ресурсы. Генерацию финальной строки выполнять используя форматирование строк.

public static string DatePicker(this HtmlHelper html, string id, string text)
{
    return String.Format(Resources.DatePicker, id, text, Resources.DaysOptions, Resources.MonthsOptions, Resources.YearsOptions);
}

В приведенном фрагменте есть заметное преимущество – централизованное управление разметкой, возможность использования разной разметки для разных культур и разделение самой разметки и кода. Можно пойти дальше и создать дополнительную обертку над ресурсами, которая будет отвечать за небольшую модификацию фрагментов кода. Однако мы на этом остановимся – мысль, я думаю, понятна.

Использование дополнительных слоев абстракции

Достигнуть большего контроля над логикой и разметкой можно за счет использования дополнительной абстракции над созданием самой разметки, вынесением отдельных методов, генерирующих повторяющие элементы и создание тегов с помощью специального класса TagBuilder. В WebForms при создании контролов (Custom Controls) используется похожий подход.

TagBuilder активно используется в расширениях, входящих в саму сборку System.Web.Mvc, в чем можно убедиться, посмотрев, например, на исходный код System .Web .MVC .SelectExtensions.

Ниже привожу код, использующий больше абстракции, нежели предыдущие. Заодно демонстрирую простую концепцию по восстановлению значений после отправки данных на сервер (метод GetValue – основной принцип сначала смотреть во ViewData, затем в параметрах запроса).

public static class DataPickerHelper
{
    private static string DAY_PREFIX = "day_";
    private static string MONTH_PREFIX = "month_";
    private static string YEAR_PREFIX = "year_";

    private static string ListItemToOption(SelectListItem item)
    {

        TagBuilder builder = new TagBuilder("option") { InnerHtml = HttpUtility.HtmlEncode(item.Text) };

        if (item.Value != null)
            builder.Attributes["value"] = item.Value;

        if (item.Selected)
            builder.Attributes["selected"] = "selected";

        return builder.ToString(TagRenderMode.Normal);

    }

private static string SelectList(string id, List<SelectListItem> items)
{
    StringBuilder listItemBuilder = new StringBuilder();

    foreach (var item in items)
    {
        listItemBuilder.AppendLine(ListItemToOption(item));
    }

    TagBuilder tagBuilder = new TagBuilder("select")
    {
        InnerHtml = listItemBuilder.ToString()
    };

    tagBuilder.Attributes.Add("id", id);
    tagBuilder.Attributes.Add("name", id);
    return tagBuilder.ToString(TagRenderMode.Normal);

}

public static string DatePicker(this HtmlHelper html, string id)
{
    return DatePicker(html, id, String.Empty);
}

public static string DatePicker(this HtmlHelper html, string id, string text)
{

// buffer
StringBuilder sb = new StringBuilder();

// generate days
List<SelectListItem> days = new List<SelectListItem>();
string dayValue = GetValue(html, DAY_PREFIX + id);

for (int i = 0; i <= 31; i++)
{
    days.Add(new SelectListItem
{

Text = (i == 0) ? String.Empty : i.ToString(),
Value = i.ToString(),
Selected = (dayValue == i.ToString())
});

}

sb.AppendLine(SelectList(DAY_PREFIX + id, days));

// generate months
List<SelectListItem> months = new List<SelectListItem>();
string monthValue = GetValue(html, MONTH_PREFIX + id);

for (int i = 0; i <= 12; i++)
{
    months.Add(new SelectListItem
    {
        Text = (i == 0) ? String.Empty : DateTimeFormatInfo.CurrentInfo.MonthNames[i - 1],
        Value = i.ToString(),
        Selected = (monthValue == i.ToString())
    });
}

sb.AppendLine(SelectList(MONTH_PREFIX + id, months));

// generate years
List<SelectListItem> years = new List<SelectListItem>();
string yearValue = GetValue(html, YEAR_PREFIX + id);

for (int i = 1900; i <= DateTime.Now.Year; i++)
{
    years.Add(new SelectListItem
{
    Text = (i == 1900) ? String.Empty : i.ToString(),
    Value = i.ToString(),
    Selected = (yearValue == i.ToString())
});

}

sb.AppendLine(SelectList(YEAR_PREFIX + id, years));

    // parent tag
    if (!String.IsNullOrEmpty(text))
    {
        TagBuilder div = new TagBuilder("div");
        div.Attributes.Add("id", id);
        div.InnerHtml = text + sb.ToString();

        sb = new StringBuilder(div.ToString(TagRenderMode.Normal));
    }

    return sb.ToString();
}

private static string GetValue(HtmlHelper html, string id)
{
    object o = null;

    if (html.ViewData != null)
        o = html.ViewData.Eval(id);

    if (o == null)
        o = html.ViewContext.RequestContext.HttpContext.Request.Params[id];

    return (o == null) ? String.Empty : o.ToString();
}

}

Я предпочитаю использовать именование XXXHelper для классов, содержащих расширения для Html, а не XXXExtenstions, поскольку такое именование используется в коде самого MVC Framework. Причина проста – во времена первых Preview версий MVC Framework, я сливал ветки кода своего приложения и MVC Framework, поскольку приходилось часто вносить изменения в сам код MVC Framework и я держал свои фрагменты кода в той же ветке. Привычка сохранилась.