Одна из фич, незаметно попавших в ASP.NET 4.5 – встроенная поддержка минификации контента. Упаковщик живет в пространстве имен System.Web.Optimization.

Упаковка контента включена по умолчанию в шаблонах ASP.NET MVC 4/.NET 4.5 в Visual Stidio 11 Beta, и, надеюсь, останется включенной по умолчанию в релизной версии.

Хоть фича и упоминается как новшество в ASP.NET 4.5, она доступна для проектов на базе .NET 4.0 и ASP.NET MVC 3. Сам по себе упаковщик доступен в виде пререлизного пакета nuget.

После установки пакета придется сделать еще несколько изменений:

1. Добавить вызов EnableDefaultBundles на старте приложения, в global.asax.cs:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    System.Web.Optimization.BundleTable.Bundles.EnableDefaultBundles();
}

2. Заменить явное подключение скриптов и стилей в _Layout.cshtml на

<link href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Content/css")" rel="stylesheet" type="text/css" />
<script src="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Scripts/js")" type="text/javascript"></script>

После этого все css и все скрипты будут подтянуты двумя файлами:

Untitled

Скриншот выше снят для стандартного шаблона ASP.NET MVC 3. Все скрипты в шаблоне весят достаточно много, отсюда и полумегабайтный размер выходного файла. Достаточно удалить лишние файлы из Scripts, и размер пакета станет более приемлимым :)

Кроме очевидного “собрать все в один большой файл”, System.Web.Optimization делает еще несколько вещей:

  • Игнорирует файлы .intellisense.js, -vsdoc.js, .debug.js.
  • Подхватывает уже минимизированные файлы – min.js, и подключает их “как есть”.
  • Учитывает порядок подключения скриптов для JQuery/UI/Validate, modernizr, dojo, mootools, prototype и ext.js

    Дополнительная плюшка, котороя не слишком заметна в документации – возможность подключения своих трансформаций для выходных файлов, на случай если стандартных JsMinify и CssMinify не хватит. Список уже готовых трансформаций виден в поиске на nuget. И в нем уже есть Less и CoffeeScript.

  • Первая часть, где мы остановились на рабочих выпадающих списках с нерабочей валидацией.

    Для того, чтобы стандартная клиентская валидация заработала, нужно сделать следующее:

    1. Заполнить метаданные. Мы сделали это в прошлой части.
    2. Заполнить поля ViewData.TemplateInfo. В нашем случае – из TemplateInfo родительского действия.
    3. Отрендерить выпадающий список на основе метаданных и данных шаблона.

    Изменения затронут только представление выпадающего списка.

    ViewsListDropDown.cshtml

    @model SelectList
    @{
       
    this
    .ViewContext.ViewData.TemplateInfo.FormattedModelValue
            =
    this
    .ViewContext.ParentActionViewContext.ViewData.TemplateInfo.FormattedModelValue;
           
       
    this
    .ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix
            =
    this
    .ViewContext.ParentActionViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
    }
    @
    Html.DropDownList(
       
    ""
    ,
       
    this
    .Model,
       
    this.ViewData.ModelMetadata.NullDisplayText
        )

    Важно: ни в коем случае нельзя подменять ссылку ViewData.TemplateInfo на объект из родительского контекста. ASP.NET MVC рендерит атрибуты валидации только для первого элемента управления с заданным именем. В нашем примере это приведет к отсутствию валидаторов у всех списков, кроме первого.

    После этого валидаторы заработают, и даже слишком хорошо:

    image

    В стандартном валидаторе целочисленного формата есть баг – он учитывает только атрибут value у элементов списка, и не учитывает innerText. Т.е. выбранный на картинке выше <option>1992</option> он числом не считает.

    Для обхода этого бага можно создать SelectList с явным указанием имен свойств для Text/Value:

    ControllersListController.cs

    public ActionResult Years()
    {
       
    int? selectedYear = this.ControllerContext.ParentActionViewContext.ViewData.Model as int
    ?;

       
    var model = new SelectList(Enumerable.Range(1990, 20).Select(y => new
    { Value = y }),
           
    "Value", "Value"
    , selectedYear);

       
    this
    .ViewData.Model = model;
       
    this.ViewData.ModelMetadata = this
    .ControllerContext.ParentActionViewContext.ViewData.ModelMetadata;

       
    return View("DropDown");
    }

    Некрасиво, но работает. Надеюсь, баг исправят в релизе :)

    Исходный код к этой статье: MvcDropDowns2.zip. Требует ASP.NET MVC 3.0 RC2 для запуска.

    …В примерах этого нет, но читатель легко это реализует сам…
    ulu, статья на хабре 

    Недавно пришлось реализовывать тот самый “smart way” отображения выпадающий списков, с поправкой на ASP.NET MVC 3. Это пост – короткий чеклист для реализации.

    Попробуем отобразить выпадающий список для нескольких полей модели.

    Основная модель:

    ModelsMovie.cs

    public class Movie
    {
       
    public int Id { get; set
    ; }
       
    public int GenreId { get; set
    ; }
       
    public int Year { get; set; }
    }

    Контроллер с двумя действиями – отображение формы и создание Movie:

    ControllersMovieController.cs

    public class MovieController : Controller
    {
        [
    HttpGet
    ]
       
    public ActionResult
    Create()
        {
           
    return
    View();
        }

        [
    HttpPost
    ]
        [
    ActionName("Create"
    )]
       
    public ActionResult CreateMovie(Movie
    model)
        {
           
    if (!this
    .ModelState.IsValid)
            {
               
    return
    View(model);
            }
           
    else
            {
               
    // TODO: save model
                // this.Repository.Save(model);

               
    return this.RedirectToAction("Details"
    ,
                   
    new
    { id = model.Id });
            }
        }

        [
    HttpGet
    ]
       
    public ActionResult
    Details()
        {
           
    // sample code to show the same Create form with preset values
            return View("Create", new Movie() { GenreId = 2, Year = 2000 });
        }
    }

    Представление для создания Movie. Создано стандартным scaffold-ом. Подключение скриптов валидации перенесено в Views/Shared/_Layout.cshtml:

    ViewsMovieCreate.cshtml

    @model MvcDropDowns.Models.Movie

    @{
        ViewBag.Title =
    "Create a Movie"
    ;
    }

    <h2>Create a Movie</h2>

    @
    using
    (Html.BeginForm()) {
        @Html.ValidationSummary(
    true
    )
       
    <fieldset>
            <legend>Movie</legend>

           
    <div class="editor-label">
                @Html.LabelFor(model => model.GenreId)
           
    </div>
            <div class="editor-field">
                @Html.EditorFor(model => model.GenreId)
                @Html.ValidationMessageFor(model => model.GenreId)
           
    </div>

           
    <div class="editor-label">
                @Html.LabelFor(model => model.Year)
           
    </div>
            <div class="editor-field">
                @Html.EditorFor(model => model.Year)
                @Html.ValidationMessageFor(model => model.Year)
           
    </div>

           
    <p>
                <input type="submit" value="Create" />
            </p>
        </fieldset>
    }

    Собираем, запускаем. Видим два текстовых поля. При нажатии на кнопку Create срабатывают клиентские валидаторы – поля не nullable.

    image

    Начнем превращать текстбоксы в выпадающие списки. Для начала, добавим UIHint и еще пару атрибутов для более человеческого отображения:

    ModelsMovie.cs

    public class Movie
    {
       
    public int Id { get; set
    ; }
           
        [
    UIHint("Genre"
    )]
        [
    Display(Name="Genre"
    )]
        [
    DisplayFormat(NullDisplayText="---Please Select---"
    )]
       
    public int GenreId { get; set
    ; }

        [
    UIHint("Year"
    )]
        [
    DisplayFormat(NullDisplayText="---Please Select---"
    )]
       
    public int Year { get; set; }
    }

    Следующий шаг – добавление шаблонов редактирования, с именами Genre и Year:

    ViewsSharedEditorTemplatesGenre.cshtml

    @model int?
    @{
        Html.RenderAction(
    "Genres", "List");
    }

    ViewsSharedEditorTemplatesYear.cshtml

    @model int?
    @{
        Html.RenderAction(
    "Years", "List");
    }

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

    ControllersListController.cs

    public class ListController : Controller
    {
       
    public ActionResult
    Genres()
        {
           
    int? selectedGenreId = this.ControllerContext.ParentActionViewContext.ViewData.Model as int
    ?;

           
    var genres = new List<Genre
    >
            {
               
    new Genre { Id = 1, DisplayName = "Comedy"
    },
               
    new Genre { Id = 2, DisplayName = "Horror"
    },
               
    new Genre { Id = 3, DisplayName = "Documentary"
    }
            };

           
    var model = new SelectList(genres, "Id", "DisplayName"
    , selectedGenreId);

           
    this
    .ViewData.Model = model;
           
    this.ViewData.ModelMetadata = this
    .ControllerContext.ParentActionViewContext.ViewData.ModelMetadata;

           
    return View("DropDown"
    );
        }

       
    public ActionResult
    Years()
        {
           
    int? selectedYear = this.ControllerContext.ParentActionViewContext.ViewData.Model as int
    ?;

           
    var model = new SelectList(Enumerable
    .Range(1990, 20), selectedYear);

           
    this
    .ViewData.Model = model;
           
    this.ViewData.ModelMetadata = this
    .ControllerContext.ParentActionViewContext.ViewData.ModelMetadata;

           
    return View("DropDown");
        }
    }

    Последний этап – добавление представления DropDown. Вызываем Html.DropDown, передавая ему все подряд:

    ViewsListDropDown.cshtml

    @model SelectList
    @{
       
    var templateInfo = this
    .ViewContext.ParentActionViewContext.ViewData.TemplateInfo;
    }
    @
    Html.DropDownList(
        templateInfo.GetFullHtmlFieldName(
    ""
    ),
       
    this
    .Model,
       
    this
    .ViewData.ModelMetadata.NullDisplayText,
       
    new
        {
            id = templateInfo.GetFullHtmlFieldId(
    "")
        })

    После запуска мы должны увидеть полноценные выпадающие списки:

    image

    У предложенной реализации есть один серьезный баг. Серверные валидаторы работают (как и раньше). Клиентские – не работают совсем. О том, как их починить, рассказано во второй части.

    Исходный код к этой статье: MvcDropDowns.zip. Требует ASP.NET MVC 3.0 RC2 для запуска.