Monday, March 31, 2014

Datapicker, Validation и другие особенности ASP.NET MVC

Здравствуйте, уважаемые читатели. В этой статье мы рассмотрим принципы работы с календарями в ASP.NET MVC4, а также валидацию данных в этих контролах. Почему, спросите вы, я решил рассмотреть пример с контролами для работы с датами? Ответ прост: с ними больше всего мороки. Как с этими проблемами бороться, мы рассмотрим в этой статье. Давайте создадим приложение ASP.NET MVC 4 Web Application, как показано на рисунке ниже, и назовем его ValidationDemo.Web.
Для того чтобы не писать самим страницы, выберем простое Internet Application приложение, на движке Razor, для того чтобы проще было продемонстрировать наш пример.
После этого перейдем в файл Index.cshtml и подправим его следующим образом:
@{
    ViewBag.Title = "Home Page";
}

<h2>Test Data Validation</h2>

<div>
    <div>
        C
        <input type="date" name="fromDate" value="@DateTime.Now"/>
    </div>
    <div>
        по
        <input type="date" name="toDate" value="@DateTime.Now"/>
    </div>
</div>
После запуска Google Chrome версии 29.0.1547.76 видим результат, аналогичный приведенному на экране.
К сожалению, этот тип не работает в версиях IE9 и Mozilla Firefox 20 версии. После посмотрев на http://caniuse.com/#search=input%20type, я понял, почему этот контрол у меня не заработал. Оказывается, все довольно просто: этот контрол не работает нормально в данных версиях браузеров.
Поэтому нужно искать другой путь для отображения контрола для вывода даты. Создадим модель данных ValidationDateControlв которой добавим два поля типа DateTime, для начальной даты и для конечной даты.
public class ValidationDateControl
{
       [DataType(DataType.Date)]
       [Display(Name = "С")]
       [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
       public DateTime DateTimeStart { get; set; }

       [DataType(DataType.Date)]
       [Display(Name = "по")]
       [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
       public DateTime DateTimeEnd { get; set; }
}
После этого перейдем в наш контроллер HomeController и изменим реализацию функции Index().
public ActionResult Index()
{
       var data = new ValidationDateControl();
       data.DateTimeStart = DateTime.Now.AddDays(-3).Date;
       data.DateTimeEnd = DateTime.Now.Date;
       return View("Index", data);
}
Затем необходимо немного модифицировать файл представления Index.cshtmlкак показано ниже.
@{
    ViewBag.Title = "Home Page";
}

<h2>Test Data Validation</h2>

@model ValidationDemo.Web.Models.ValidationDateControl

<div>
    <div>
        @Html.LabelFor(m=>m.DateTimeStart)
        @Html.TextBox("fromDate", Model.DateTimeStart.Date.ToString("yyyy-MM-dd"), new { @class = "text" })
    </div>
    <div>
        @Html.LabelFor(m=>m.DateTimeEnd)
        @Html.TextBox("toDate", Model.DateTimeEnd.Date.ToString("yyyy-MM-dd"), new { @class = "text" })
    </div>
</div>
В примере мы использовали возможности движка Razor. Для того чтобы более детально ознакомиться с html-представлением, которое будет сгенерировано нашим движком, мы можем посмотреть на вот этот хелп: HTML Helpers For Forms In Razor Web Pages. Но чтобы понять, что здесь написано, приведу ниже трансляцию с Razor в html-разметку.
На данном этапе у вас не должно возникнуть сложностей. После запуска тестового веб-приложения вы увидите просто текстовое поле с введенной датой, но у нас не будет возможности изменить эту дату, кроме ручного ввода в поле. Поэтому для этого поля воспользуемся контролом datapicker с библиотеки jquery-ui.
Для того чтобы воспользоваться этим контролом, перейдем в файл Index.cshtml и добавим следующий скрипт:
<script src="~/Scripts/jquery-1.8.2.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        var txt = $('.text');
        txt.datepicker({
            dateFormat: "yy-mm-dd"
        });
    });
</script>
Полный исходный код будет выглядеть следующим образом:
@{
    ViewBag.Title = "Home Page";
}

<h2>Test Data Validation</h2>

<script src="~/Scripts/jquery-1.8.2.js"></script>

@model ValidationDemo.Web.Models.ValidationDateControl

<div>
    <div>
        @Html.LabelFor(m=>m.DateTimeStart)
        @Html.TextBox("fromDate", Model.DateTimeStart.Date.ToString("yyyy-MM-dd"), new { @class = "text" })
    </div>
    <div>
        @Html.LabelFor(m=>m.DateTimeEnd)
        @Html.TextBox("toDate", Model.DateTimeEnd.Date.ToString("yyyy-MM-dd"), new { @class = "text" })
    </div>
</div>

<script type="text/javascript">
    $(document).ready(function () {
        var txt = $('.text');
        txt.datepicker({
            dateFormat: "yy-mm-dd"
        });
    });
</script>
После этого нам необходимо перейти в файл _Layout.cshtml и добавить следующую строчку @Scripts.Render("~/bundles/jqueryui") после строки @Scripts.Render("~/bundles/jquery")
Ниже приведена часть реализации.
       </footer>

        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/jqueryui")
        @RenderSection("scripts", required: false)
    </body>
</html>
После проделанной работы можем запустить пример и посмотреть результат.
Как видим с рисунка выше, контрол подтянулся, но не подтянулись сами css стили. У меня почему-то возникли проблемы со стилями для файла jquery-ui-1.8.24.js, и я решил их обновить до более новой версии через Manage NuGet Packages к доступной версии 1.10.4, а также заодно обновить библиотеку jQuery до версии 2.1.0.
После обновления заходим в файл _Layout.cshtml и добавленные строки @Scripts.Render("~/bundles/jqueryui") и строку @Scripts.Render("~/bundles/jquery") перемещаем в самый верх. Также необходимо добавить стили.
@Styles.Render("~/Content/css")
@Styles.Render("~/Content/themes/base/css")
       
@Scripts.Render("~/bundles/modernizr")

@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryui")
Полная реализация приведена ниже.
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>@ViewBag.Title - Статистика Арт-Звіт</title>
        <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
        <meta name="viewport" content="width=device-width" />
        @Styles.Render("~/Content/css")
        @Styles.Render("~/Content/themes/base/css")
       
        @Scripts.Render("~/bundles/modernizr")

        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/jqueryui")

    </head>
    <body>
        <header>
            <div class="content-wrapper">
                @*<div class="float-left">
                    <p class="site-title">@Html.ActionLink("your logo here", "Index", "Home")</p>
                </div>*@
                <div class="float-right">
                    @*<section id="login">
                        @Html.Partial("_LoginPartial")
                    </section>*@
                    <nav>
                        <ul id="menu">
                            <li>@Html.ActionLink("Графік", "Chart", "Chart")</li>
                            <li>@Html.ActionLink("Статистика", "Statistic", "Statistic")</li>
                        </ul>
                    </nav>
                </div>
            </div>
        </header>
        <div id="body">
            @RenderSection("featured", required: false)
            <section class="content-wrapper main-content clear-fix">
                @RenderBody()
            </section>
        </div>
        <footer>
            <div class="content-wrapper">
                <div class="float-left">
                    <p>&copy; @DateTime.Now.Year - Панель статистики</p>
                </div>
            </div>
        </footer>
        @RenderSection("scripts", required: false)
    </body>
</html>
По сути, мы с вами выполнили всю черновую работу.  Осталось только изменить старый скрипт в файле Index.cshtml на новый.
<script src="~/Scripts/jquery-ui-1.10.4.js"></script>
Кроме этой строки, больше ничего изменять не нужно. Запускаем наше приложение в IE для примера и смотрим на результат.
Поскольку контрол мы уже добавили, а также заставили его нормально отображаться, осталось напоследок добавить валидацию для этих контролов. Для этого в нашу форму добавим кнопку, после нажатия на которую через POST запрос отправим данные на валидацию. После небольших изменений на форме ее код будет выглядеть следующим образом:
@{
    ViewBag.Title = "Home Page";
}

@model ValidationDemo.Web.Models.ValidationDateControl

<h2>Test Data Validation</h2>

<script src="~/Scripts/jquery-ui-1.10.4.js"></script>

@using (Html.BeginForm())
{
    <div>
        <div>
            @Html.LabelFor(m => m.DateTimeStart)
            @Html.TextBox("DateTimeStart", Model.DateTimeStart.Date.ToString("yyyy-MM-dd"), new {@class = "text"})
            @if (!ViewData.ModelState.IsValid)
            {
                if (@ViewData.ModelState["DateTimeStart"].Errors.Count > 0)
                {
                    <span class="field-validation-error">
                        @ViewData.ModelState["DateTimeStart"].Errors[0].ErrorMessage
                    </span>
                }
            }
        </div>
        <div>
            @Html.LabelFor(m => m.DateTimeEnd)
            @Html.TextBox("DateTimeEnd", Model.DateTimeEnd.Date.ToString("yyyy-MM-dd"), new {@class = "text"})
            @if (!ViewData.ModelState.IsValid)
            {
                if (@ViewData.ModelState["DateTimeEnd"].Errors.Count > 0)
                {
                    <span class="field-validation-error">
                        @ViewData.ModelState["DateTimeEnd"].Errors[0].ErrorMessage
                    </span>
                }
            }
        </div>
       
        <div>
            <input type="submit" name="calculate" value="Calculate"/>
        </div>
    </div>
}

<script type="text/javascript">
    $(document).ready(function () {
        var txt = $('.text');
        txt.datepicker({
            dateFormat: "yy-mm-dd"
        });
    });
</script>
Переходим в наш контроллер HomeController.cs и добавляем валидацию на пост запрос.
[HttpPost]
public ActionResult Index(ValidationDateControl model)
{
       if (model.DateTimeStart > DateTime.Now.Date)
       {
             ModelState.AddModelError("DateTimeStart", "Начальная дата не может быть больше чем текущая");
       }

       if (model.DateTimeEnd > DateTime.Now.Date)
       {
             ModelState.AddModelError("DateTimeEnd", "Конечная дата не может быть больше чем текущая");
       }

       if (model.DateTimeEnd < model.DateTimeStart)
       {
             ModelState.AddModelError("DateTimeStart", "Начальная дата не может быть больше конечной");
       }

       var result = model.DateTimeEnd - model.DateTimeStart;

       if (result.Days > 31)
       {
             ModelState.AddModelError("DateTimeStart", "Разница между начальной и конечной датой не может превышать 31 день");
       }
       return View("Index", model);
}
Для примера запустим наше веб-приложение и поставим дату начала больше, чем конечную дату. Результат вы можете увидеть на рисунке ниже.
Пример 1
Пример 2
Пример 3

Итоги
В приведенной статье мы рассмотрели, как можно подружить ASP.NET MVC 4 с jQuery UI для работы с датами, а также как можно для этих контролов повесить валидацию данных. Надеюсь, что после прочтения данной статьи у вас не возникнет проблем с данными контролами и с валидацией в ASP.NET MVC 4 как таковой. Поскольку мне пришлось в свое время разбирать эту тему и найти информацию обо всем в отдельном месте, мне не посчастливилось, делюсь своими знаниями, которые приобрел, разбираясь с этой непростой темой. Надеюсь, что статья будет вам полезной.