Мобильная версия интернет-магазина

102 Исходный код проекта

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

Суть в том, что обеспечение качественного интерфейса мобильным пользователям является трудоемкой задачей, которая намного сложнее доставки содержимого пользователям настольных систем. Это требует тщательного планирования, проектирования и обширного тестирования, но даже после всех мероприятий довольно легко потерпеть неудачу с новым смартфоном или планшетом.

Разработка мобильных веб-приложений

В MVC Framework имеется ряд средств, которые могут способствовать упрощению разработки мобильных веб-приложений. Но MVC Framework является инфраструктурой серверной стороны, которая получает HTTP-запросы и генерирует HTML-ответы, и она располагает ограниченным объемом функциональности по сравнению с широким диапазоном возможностей, встречаемых при ориентации на мобильных клиентов. Уровень помощи, предлагаемой MVC Framework, зависит от принятой у вас стратегии мобильности. Существуют три стратегии мобильности, которые описаны в последующих разделах.

Ничего не делать (или предпринимать минимально возможные действия)

Идея о том, чтобы ничего не делать, может выглядеть странной, однако некоторые мобильные устройства способны поддерживать содержимое, разработанное для настольных клиентов. Многие (правда, самые последние) мобильные устройства располагают экранами с высоким разрешением и плотностью, достаточным объемом памяти и браузерами, которые могут с высокой скоростью визуализировать HTML-разметку и выполнять код JavaScript.

Если ваше приложение не слишком требовательное, вы можете обнаружить, что многие мобильные устройства без проблем отображают все содержимое, генерируемое приложением. Например, на рисунке ниже показано, что устройство Asus Nexus 7 отображает приложение GameStore безо всяких его модификаций:

Отображение приложения GameStore в Asus Nexus 7

Все выглядит вполне нормально. Единственная проблема в том, что страничные ссылки в нижней части страницы не уместились, но это легко исправить, изменив компоновку страницы или количество отображаемых на странице товаров.

Все экранные снимки, приведенные в этой статье, получены с использованием средств программы Opera Mobile Emulator, которую вы можете загрузить по адресу opera.com/ru/developer/mobile-emulator.

Использование чувствительного дизайна

Следующая стратегия предусматривает создание содержимого, которое подстраивается под возможности устройства, где оно отображается. Такая стратегия называется чувствительным дизайном. Стандарт CSS располагает средствами, которые позволяют изменять стилизацию, применяемую к элементам, на базе возможностей устройства. Этот прием наиболее часто используется для переключения компоновки содержимого на основе ширины экрана.

Чувствительный дизайн - это то, что обрабатывается клиентом с применением CSS и не управляется напрямую MVC Framework. С целью демонстрации этого подхода (и ряда соображений, касающихся MVC Framework) в этой статье будут применяться средства чувствительного дизайна, включенные в библиотеку Bootstrap, которая используется для стилизации приложения GameStore (и является одной из библиотек, включенных в шаблоны проектов MVC 5 среды Visual Studio 2013).

Задача состоит в том, чтобы приспособить компоновку главной части приложения, обеспечив ее видимость на мобильном устройстве. Стратегия "ничего не делать" для такого устройства не подойдет, поскольку оно имеет узкий экран:

Отображение приложения GameStore в смартфоне

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

Инфраструктура MVC Framework не является активным участником чувствительного дизайна. Она отправляет всем браузерам одно и то же содержимое и позволяет им самостоятельно выяснять, что именно они должны отображать. Это означает отсутствие разумного способа добавления модульных тестов для чувствительного дизайна в проекте Visual Studio. Такой подход требует тщательного тестирования на стороне клиента, которое трудно автоматизировать.

Создание чувствительного заголовка

Мы планируем начать с заголовка страницы, который содержит название приложения GameStore, сводку по корзине и кнопку "Заказать". Хотя простейшее решение предполагало бы удаление названия и освобождение достаточного пространства для остального содержимого, мы собираемся сохранить его и переупорядочить другое содержимое в две строки.

В примере ниже приведено настроенное содержимое заголовка в файле _Layout.cshtml из проекта GameStore.WebUI:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="~/Content/bootstrap.css" rel="stylesheet" />
    <link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
    <link href="~/Content/ErrorStyles.css" rel="stylesheet" />
    <title>@ViewBag.Title</title>
    <style>
        .navbar-right {
            float: right !important;
            margin-right: 15px;
            margin-left: 15px;
        }
    </style>
</head>
<body>
    <div class="navbar navbar-inverse" role="navigation">
        <a class="navbar-brand" href="#">
            <span class="hidden-xs">GameStore - магазин компьютерных игр</span>
            <div class="visible-xs">Game</div>
            <div class="visible-xs">Store</div>
        </a>
        @Html.Action("Summary", "Cart")
    </div>
    <div class="row panel">
        <div id="categories" class="col-xs-3">
            @Html.Action("Menu", "Nav")
        </div>
        <div class="col-xs-8">
            @RenderBody()
        </div>
    </div>
</body>
</html>

В библиотеке Bootstrap определен набор классов, которые могут использоваться для отображения или сокрытия элементов на основе ширины экрана устройства. Это то, что можно делать вручную с помощью медиа-запросов CSS, но классы Bootstrap интегрированы в другие стили.

Для заголовка "GameStore - магазин компьютерных игр" применяются классы visible-xs и hidden-xs, позволяющие переключаться на текст в двух строках, который будет отображаться вертикально, когда размер окна меньше 768 пикселей.

Библиотека Bootstrap предоставляет пары классов, отображающие или скрывающие элементы при различных размерах окон браузеров, имена которых начинаются с visible- или hidden-. В примере используются классы "xs" (т.е. visible-xs и hidden-xs). Классы "sm" работают с окнами, которые шире 768 пикселей, классы "md" - с окнами шире 992 пикселей, а классы "lg" - с окнами шире 1200 пикселей.

Средства чувствительного дизайна CSS, подобные предоставляемым Bootstrap, основаны на размере окна браузера, а не экрана устройства. Браузеры мобильных устройств обычно отображаются в полноэкранном режиме, т.е. размеры окна и экрана совпадают, однако на это нельзя полагаться абсолютно во всех случаях. Как всегда, целевые устройства необходимо проверять, чтобы не оказалось, что сделаны предположения, которые привели к неудаче.

Чтобы просмотреть результаты внесенных изменений, запустите приложение и отобразите список товаров в обычном настольном браузере, который обладает тем преимуществом, что позволяет изменять размер окна. Сделайте окно небольшим (меньше 786 пикселей), и вы увидите, что текст заголовка сокращается и разбивается на две строки:

Использование средств чувствительного дизайна Bootstrap для настройки заголовка

Это может выглядеть как незначительное изменение, но оно оказывает крупное влияние на маленьких экранах, особенно в комбинации с изменениями, внесенными в файл Views/Cart/Summary.cshtml, который является представлением, отображающим сводку по корзине и ее содержимое. Указанные изменения можно видеть в примере ниже:

@model GameStore.Domain.Entities.Cart

<div class="navbar-right hidden-xs">
    @Html.ActionLink("Заказать", "Index", "Cart",
    new { returnUrl = Request.Url.PathAndQuery },
    new { @class = "btn btn-default navbar-btn" })
</div>

<div class="navbar-right visible-xs">
    <a href=@Url.Action("Index", "Cart", new { returnUrl = Request.Url.PathAndQuery })
       class="btn btn-default navbar-btn">
        <span class="glyphicon glyphicon-shopping-cart"></span>
    </a>
</div>

<div class="navbar-text navbar-right">
    <b class="hidden-xs">Ваша корзина:</b>
    @Model.Lines.Sum(x => x.Quantity) игр,
    @Model.ComputeTotalValue().ToString("# руб")
</div>

Это тот же самый прием, который применялся в файле _Layout.cshtml, где выборочно отображалось и скрывалось содержимое. Однако в данном случае на маленьком экране скрывается стандартная кнопка "Заказать" и взамен отображается кнопка в виде значка с использованием одного из значков, включенных в состав пакета Bootstrap.

Значки Bootstrap применяются посредством элемента <span>, а это означает невозможность использования вспомогательного метода Html.ActionLink(), поскольку он не позволяет устанавливать содержимое создаваемого им элемента. Вместо этого элемент <a> определяется напрямую, а с применением вспомогательного метода Url.Action(), генерируется URL для атрибута href. В результате получается элемент <a> с теми же атрибутами, которые создал бы метод Html.ActionLink(), но он содержит элемент <span>.

Эффект от внесенных изменений можно видеть на рисунке ниже, где показано содержимое заголовка, отображаемого в HTC Desire:

Модифицированный заголовок приложения GameStore, отображаемый в эмуляторе HTC Desire

Сравнение стилей разработки "сначала мобильная версия" и "сначала настольная версия"

Большинство проектов веб-приложений начинаются с создания настольных клиентов и последующего добавления поддержки для мобильных клиентов, в точности как это делается с нашим интернет-магазином. Такой подход называется проектированием/разработкой в стиле "сначала настольная версия". Ему присуща общая проблема: к моменту начала работ над мобильным клиентом разработка на серверной стороне почти завершена, в результате чего получается неуклюжий мобильный интерфейс, с трудом взаимодействующий с функциональностью, которая ориентирована на более мощных настольных клиентов.

Существует альтернативная философия под названием проектирование/разработка в стиле "сначала мобильная версия", при которой в качестве основы приложения создается мобильный клиент и затем добавляются функциональные средства, предназначенные для на более мощных настольных браузеров. Другими словами, подход "сначала настольная версия" предусматривает начало с полного набора функций с последующим изящным понижением возможностей для менее мощных устройств, тогда как подход "сначала мобильная версия" предполагает начало с небольшого набора функций, который изящно расширяется для более мощных устройств.

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

Я склонен работать в рамках тесно связанного цикла написание-компиляция-проверка (который означает частую перезагрузку URL в браузере), и меня не устраивают сложности, которые приходится преодолевать, чтобы обеспечить аналогичный цикл на мобильном устройстве.

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

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

Создание чувствительного списка товаров

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

В примере ниже приведена соответствующая модификация файла _Layout.cshtml:

...
<div class="row panel">
        <div id="categories" class="col-sm-3 hidden-xs">
            @Html.Action("Menu", "Nav")
        </div>
        <div class="col-xs-12 col-sm-8">
            @RenderBody()
        </div>
</div>
...

В компоновке находится только один вызов метода RenderBody(). Результатом этого ограничения является невозможность иметь дублированные наборы отображаемых и скрываемых элементов, в каждом из которых содержится вызов RenderBody(). Вместо этого придется изменять компоновку экранной сетки, которая содержит вызов метода RenderBody(), чтобы элементы в компоновке адаптировались под содержимое представления.

Одна из причин использования экранной сетки из библиотеки Bootstrap для структуризации содержимого внутри файла _Layout.cshtml объясняется тем, что она включает ряд средств чувствительного дизайна, которые позволяют обойти ограничение на один вызов RenderBody(). Компоновка сетки Bootstrap поддерживает 12 колонок. Количество занимаемых элементов указывается с применением класса вроде показанного ниже:

<div class="col-xs-8">
     @RenderBody()
</div>

Подобно описанным ранее классам "hidden-" и "visible-", библиотека Bootstrap предоставляет набор классов, которые устанавливают количество колонок, занимаемых содержимым внутри экранной сетки, на основе ширины окна.

Классы "col-xs-" фиксированы и не изменяются на базе ширины экрана. Применение класса col-xs-8 сообщает Bootstrap о том, что элемент <div> должен охватывать 8 из 12 доступных колонок, а видимость этого элемента не должна изменяться на основе ширины окна. Классы "col-sm-" устанавливают колонки, когда окно имеет ширину 768 пикселей или больше, классы "col-md-" работают с окнами в 992 пикселя или шире и, наконец, классы "col-lg-" имеют дело с окнами, которые обладают шириной 1200 пикселей и более.

Имея все это в виду, вот классы, которые были применены к элементу <div>, окружающему вызов RenderBody(), в примере ниже:

<div class="col-xs-12 col-sm-8">
    @RenderBody()
</div>

В результате применения обоих классов элемент <div> будет занимать все 12 колонок сетки по умолчанию и 8 колонок, когда экран имеет ширину 768 пикселей и более. Другие колонки сетки содержат кнопки категорий:

<div id="categories" class="col-sm-3 hidden-xs">
    @Html.Action("Menu", "Nav")
</div>

Этот элемент будет занимать 3 колонки, когда экран шире 768 пикселей, и скрываться в противном случае. В комбинации с остальными примененными классами, описания товаров заполняют небольшие окна и делят доступное пространство с кнопками категорий в случае окон крупных размеров. Обе компоновки можно видеть на рисунке ниже. Здесь использовался настольный браузер, т.к. он позволяет легко изменять ширину окна:

Использование чувствительной экранной сетки в компоновке для списка товаров

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

Мы не хотим лишать мобильных пользователей возможности фильтрации товаров, а это значит, что категории понадобится представлять разными способами. Для этого в папке Views/Nav создается новое представление по имени MenuHorizontal.cshtml с содержимым, приведенным в примере ниже:

@model IEnumerable<string>

<div class="btn-group btn-group-sm btn-group-justified">
    @Html.ActionLink("Домой", "List", "Game", null, new { @class = "btn btn-default btn-sm" })

    @foreach (var link in Model)
    {
        @Html.RouteLink(link, new
        {
            controller = "Game",
            action = "List",
            category = link,
            page = 1
        }, new
        {
            @class = "btn btn-default btn-sm"
                    + (link == ViewBag.SelectedCategory ? " btn-primary" : "")
        })
    }
</div>

Это вариация первоначальной компоновки Menu.cshtml, но с контейнерным элементом div и несколькими классами Bootstrap для создания горизонтальной компоновки кнопок. Базовая функциональность осталась такой же. Мы генерируем набор ссылок, которые будут фильтровать товары по категориям.

Набор кнопок категорий генерируется с помощью метода действия Menu() контроллера Nav, который необходимо модифицировать, чтобы он выбирал подходящий файл представления на основе требуемой ориентации кнопок, как показано в примере ниже:

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using GameStore.Domain.Abstract;

namespace GameStore.WebUI.Controllers
{
    public class NavController : Controller
    {
        // ...

        public PartialViewResult Menu(string category = null, bool horizontalNav = false)
        {
            ViewBag.SelectedCategory = category;

            IEnumerable<string> categories = repository.Games
                .Select(game => game.Category)
                .Distinct()
                .OrderBy(x => x);

            string viewName = horizontalNav ? "MenuHorizontal" : "Menu";
            return PartialView(viewName, categories);
        }
	}
}

В методе действия Menu() определен новый параметр, который описывает ориентацию и применяется для указания ориентации, используемой при выборе имени файла представления с целью его передачи методу PartialView(). Чтобы установить значение этого параметра, необходимо возвратиться к файлу _Layout.cshtml:

...
<body>
    <div class="navbar navbar-inverse" role="navigation">
        <a class="navbar-brand" href="#">
            <span class="hidden-xs">GameStore - магазин компьютерных игр</span>
            <div class="visible-xs">Game</div>
            <div class="visible-xs">Store</div>
        </a>
        @Html.Action("Summary", "Cart")
    </div>
	
    <div class="visible-xs">
        @Html.Action("Menu", "Nav", new { horizontalNav = true })
    </div>
	
    <div class="row panel">
        <div id="categories" class="col-sm-3 hidden-xs">
            @Html.Action("Menu", "Nav")
        </div>
        <div class="col-xs-12 col-sm-8">
            @RenderBody()
        </div>
    </div>
</body>
...

В необязательном третьем аргументе метода Html.Action() можно передавать объект, позволяющий устанавливать значения для системы маршрутизации. Эта возможность применяется для указания контроллеру на то, какое представление должно быть выбрано. Общий эффект от внесенных изменений показан на рисунке ниже:

Переделанный список товаров для небольших экранов

Как видите, перенос кнопок в местоположение выше списка товаров приводит к появлению достаточного пространства для подходящего отображения каждого товара. Можно было бы продолжить улучшать внешний вид представлений, но саму идею вы наверняка уже уловили.

Помимо краткой демонстрации использования чувствительных классов CSS нужно рассмотреть некоторые ограничения, накладываемые инфраструктурой MVC Framework (такие как ограничение относительно единственного вызова метода RenderBody()), и определенные возможности, которые она может предложить для содействия в генерации содержимого разными способами (например, передача данных из представления контроллеру через систему маршрутизации и вспомогательный метод Html.Action()).

Устранение дублирования представлений

В предыдущем примере необходимо было продемонстрировать контроллер, который выбирает представление на основе информации о маршрутизации, передаваемой вызову вспомогательного метода Html.Action(). Это важная и удобная возможность, но она не будет использоваться в реальном проекте, потому что требует наличия двух представлений, Menu.cshtml и MenuHorizontal.cshtml, которые содержат очень похожую разметку и выражения Razor. В результате возникают риски при сопровождении, т.к. любые изменения в кнопках фильтрации по категориям должны применяться в двух местах.

Чтобы решить эту проблему, мы объединим представления. Для этого в папке Views/Nav мы создадим файл по имени FlexMenu.cshtml с содержимым, приведенным в примере ниже:

@model IEnumerable<string>

@{
    bool horizontal = ((bool)(ViewContext.RouteData.Values["horizontalNav"] ?? false));
    string wrapperClasses = horizontal ? "btn-group btn-group-sm btn-group-justified" : null;
}

<div class="@wrapperClasses">

    @Html.ActionLink("Домой", "List", "Game", null,
    new
    {
        @class = horizontal ? "btn btn-default btn-sm" :
            "btn btn-block btn-default btn-lg"
    })

    @foreach (var link in Model)
    {
        @Html.RouteLink(link, new
        {
            controller = "Game",
            action = "List",
            category = link,
            page = 1
        }, new
        {
            @class = (horizontal ? "btn btn-default btn-sm"
                : "btn btn-block btn-default btn-lg")
                + (link == ViewBag.SelectedCategory ? " btn-primary" : "")
        })
    }

</div>

Платой за устранение дублирования становится более сложное представление, которое может генерировать наборы кнопок с обеими ориентациями. Какой подход принять, зависит от личных предпочтений. Если вы стремитесь избегать дублирования, то приведенный пример поможет понять, какие средства для этого применяются к представлениям.

Первая возможность касается возможности доступа к информации о маршрутизации напрямую из представления. Свойство ViewContext предоставляет сведения о текущем состоянии обрабатываемого запроса, включая детали информации о маршрутизации:

bool horizontal = ((bool)(ViewContext.RouteData.Values["horizontalNav"] ?? false));

Вторая возможность связана с созданием локальных переменных внутри представления. Это доступно из-за того, что представления Razor компилируются в классы, а в примере была создана локальная переменная по имени horizontal, которая позволит не проверять данные маршрута повсеместно в коде с целью выяснения того, на какую ориентацию рассчитано представление.

Локальные переменные должны использоваться умеренно, поскольку они довольно быстро приводят к получению представлений, трудных в сопровождении и тестировании, но их уместно применять в ситуациях вроде продемонстрированной выше, т.к. с ними сопряжена вполне приемлемая цена за упрощение представления.

Связанной возможностью является способ, которым механизм Razor будет условно устанавливать атрибуты на базе переменных. В представлении была определена строка с именами классов в виде локальной переменной:

string wrapperClasses = horizontal ? "btn-group btn-group-sm btn-group-justified" : null;

Значением переменной wrapperClasses будет либо строка с именами классов, которые используются для горизонтальных компоновок, либо null. Эта переменная применяется в атрибуте class следующим образом:

<div class="@wrapperClasses">

Когда эта переменная равна null, механизм Razor достаточно интеллектуален, чтобы полностью удалить атрибут class из элемента div, генерируя элемент, подобный показанному ниже:

<div>

Когда переменная имеет значение, отличное от null, механизм Razor вставляет это значение и оставляет атрибут class незатронутым, давая такой результат:

<div class="btn-group btn-group-sm btn-group-justified">

Это хороший способ сопоставления характеристик C# с семантикой HTML, который на практике исключительно удобен при написании сложных представлений, поскольку он не вставляет значения null в атрибуты и не генерирует пустых атрибутов, что могло бы вызвать проблемы с селекторами CSS (и JavaScript-библиотеками, использующими атрибуты для выбора элементов, например, jQuery).

Условные атрибуты будут работать с любой переменной, а не только с теми, которые определены внутри представления. Это значит, что данную возможность можно применять к свойствам модели и средству ViewBag.

Для использования консолидированного представления необходимо переделать метод действия Menu() в контроллере Nav, как показано в примере ниже:

// ...
public PartialViewResult Menu(string category = null)
{
    ViewBag.SelectedCategory = category;

    IEnumerable<string> categories = repository.Games
        .Select(game => game.Category)
        .Distinct()
        .OrderBy(x => x);

    return PartialView("FlexMenu", categories);
}
// ...

Здесь был удален параметр, получающий ориентацию, и изменен вызов метода PartialView(), чтобы всегда выбиралось представление FlexMenu. В результате внесенных изменений компоновка содержимого или эффект чувствительного дизайна не изменились, однако было устранено дублирование, а это значит, что представления Menu.cshtml и MenuHorizontal.cshtml могут быть удалены из проекта Visual Studio. Кнопки фильтрации по категориям с обеими ориентациями теперь генерируются представлением FlexMenu.cshtml.

Ограничения чувствительного дизайна

С чувствительным дизайном, как способом поддержки мобильных клиентов, связан ряд проблем. Первая заключается в том, что приходится дублировать много содержимого и отправлять его серверу, чтобы оно могло отображаться в разных сценариях. Вы видели это в предыдущем разделе, когда HTML-разметка, генерируемая компоновкой, содержала множество наборов элементов для заголовка страницы и кнопок фильтрации по категориям. Дополнительные элементы не занимают слишком большой объем в каждом запросе, но общим эффектом в загруженном приложении будет отчетливый рост полосы пропускания, которую придется обеспечить, с соответствующим увеличением эксплуатационных расходов.

Вторая проблема состоит в том, что чувствительный дизайн может требовать кропотливой работы и бесконечного тестирования, чтобы полностью его наладить. Не все устройства поддерживают лежащие в основе средства CSS, которые делают возможным чувствительный дизайн (известные как медиа-запросы), должным образом. Несмотря на полноту и аккуратность, в итоге получается приложение, предоставляющее адекватный интерфейс на всех устройствах, который не является превосходным ни на одном из них и выглядит банальным из-за усреднения индивидуальных особенностей всех устройств.

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

Создание содержимого, специфичного для мобильных устройств

Чувствительный дизайн обеспечивает доставку того же самого содержимого на все устройства и использует CSS для выяснения того, как именно содержимое должно быть представлено - процесс, который не вовлекает серверную часть приложения и предполагает, что вы хотите трактовать все устройства как вариацию на одну и ту же базовую тему. Альтернативный подход предусматривает участие сервера для доступа к возможностям клиентского браузера и отправки разной HTML-разметки клиентам различных видов. Это хорошо работает, если необходимо представить совершенно отличающийся аспект настольного приложения, скажем, на планшете.

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

В инфраструктуре MVC Framework доступно средство, называемое режимами отображения, которое позволяет создавать разные представления на основе клиента, сделавшего запрос - средство, предлагаемое платформой ASP.NET. В приложении GameStore будет применяться простейшая их форма, предусматривающая трактовку всех мобильных устройств как одинаковых. Цель будет заключаться в доставке интерфейса на мобильные устройства с использованием библиотеки jQuery Mobile, при этом сохраняя существующее содержимое для настольных устройств.

Создание мобильной компоновки

Чтобы получить содержимое, специфичное для мобильных устройств, необходимо создать представления и компоновки, которые имеют суффикс ".Mobile.cshtml". Создайте в папке Views/Shared новый файл компоновки по имени _Layout.Mobile.cshtml с содержимым, приведенным в примере ниже (Поскольку имя представления содержит дополнительную точку, для создания представления придется щелкнуть правой кнопкой мыши на папке Shared и выбрать в контекстном меню пункт Add --> MVC 5 Layout Page (Razor) (Добавить --> Страница компоновки MVC 5 (Razor))):

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet"
          href="http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.css" />
    <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.3.2/jquery.mobile-1.3.2.min.js"></script>
    <title>@ViewBag.Title</title>
</head>
<body>
    <div data-role="page" id="page1">
        <div data-theme="a" data-role="header" data-position="fixed">
            <h3>GameStore</h3>
            @Html.Action("Menu", "Nav")
        </div>
        <div data-role="content">
            <ul data-role="listview" data-divider-theme="b" data-inset="false">
                @RenderBody()
            </ul>
        </div>
    </div>
</body>
</html>

В этой компоновке используется библиотека jQuery Mobile, которая подключается через сеть доставки содержимого (content delivery network - CDN), так что устанавливать пакет NuGet для нужных файлов JavaScript и CSS не понадобится.

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

Инфраструктура MVC Framework будет автоматически идентифицировать мобильных клиентов и применять при визуализации представлений файл _Layout.Mobile.cshtml, гладко заменяя им файл _Layout.cshtml, который используется для других клиентов. Результат показан на рисунке ниже:

Результат создания мобильной компоновки в приложении GameStore

Легко заметить, что компоновка отличается, но в целом наблюдается беспорядок. Это объясняется тем, что пока еще не созданы мобильные версии главного представления, обрабатывающего запрос, и частичного представления, которое применяется для кнопок фильтрации по категориям.

Создание мобильных представлений

Мы начнем с фильтрации по категориям, создав в папке Views/Nav файл представления по имени FlexMenu.Mobile.cshtml с содержимым, которое приведено в примере ниже:

@model IEnumerable<string>

<div data-role="navbar">
    <ul>
        @foreach (var link in Model)
        {
            <li>
                @Html.RouteLink(link, new
                {
                    controller = "Game",
                    action = "List",
                    category = link,
                    page = 1
                }, new
                {
                    data_transition = "fade",
                    @class = (link == ViewBag.SelectedCategory ? "ui-btn-active" : null)
                })
            </li>
        }
    </ul>
</div>

В этом представлении с помощью Razor-выражения foreach генерируются элементы <li> для категорий товаров, давая в результате элементы, которые организованы именно так, как библиотека jQuery Mobile ожидает от панели навигации, располагаемой в верхней части страницы. Итоговое представление показано на рисунке ниже:

Результат создания представления, специфичного для мобильных устройств

При форматировании элементов библиотека jQuery Mobile полагается на атрибуты данных. Атрибуты данных снабжаются префиксом data- и на протяжении многих лет были неофициальным способом определения специальных атрибутов, пока не стали официальной частью стандарта HTML5. В примере выше к элементам <li> необходимо было добавить атрибут data-transition, но использовать data-transition в качестве имени свойства для анонимного объекта нельзя, поскольку оно должно быть выражением C#. Проблема связана с символом дефиса, и механизм Razor обходит ее, транслируя символы подчеркивания внутри имен свойств в символы дефиса внутри имен атрибутов, так что можно указать data_transition в примере ниже и получить в генерируемых элементах атрибут data-transition!

В информации о товаре по-прежнему присутствует беспорядок, но кнопки категорий теперь генерируются новым представлением, специфичным для мобильных устройств. Полезно ознакомиться с тем, какие действия MVC Framework предпринимает, чтобы получить содержимое, показанное на рисунке выше.

HTTP-запрос, сделанный в браузере, нацелен на метод действия List() из контроллера Game, который сообщает MVC Framework о необходимости визуализации файла представления List.cshtml. Инфраструктуре MVC Framework известно, что запрос поступил из мобильного браузера, поэтому она начинает поиск представлений, специфичных для мобильных устройств. Поскольку файл List.Mobile.cshtml не существует, обрабатывается файл List.cshtml. Это представление опирается на файл _Layout.cshtml, но MVC Framework обнаруживает его мобильную версию и применяет вместо _Layout.cshtml файл _Layout.Mobile.cshtml. Данная компоновка требует файла FlexMobile.cshtml, однако для него также предусмотрена мобильная версия, и процесс продолжается в том же духе.

В конечном итоге ответ для браузера генерируется из смеси мобильных и общих представлений, при этом инфраструктура MVC Framework использует более специфичный файл представления, если он доступен, и гладко обходит его, если это не так.

Показанный пример предназначен для демонстрации способа доставки инфраструктурой MVC Framework содержимого, специфичного для мобильных устройств, но было бы упущением не указать на две потенциально серьезные проблемы, которые данный пример привносит в приложение GameStore.

Ни одна из указанных проблем не препятствует развертыванию приложения, но пользователи мобильной версии веб-приложения могут испытывать разочарование. Мобильные устройства будут крупной частью любого современного веб-приложения, поэтому вы должны принять все меры по доставке этой категории потребителей качественного пользовательского интерфейса.

Последнее изменение касается создания мобильной версии представления, которое генерирует сводку по товару. Для этого в папке Views/Shared создается файл представления по имени GameSummary.Mobile.cshtml с содержимым, показанным в примере ниже:

@model GameStore.Domain.Entities.Game

<div data-role="collapsible" data-collapsed="false" data-content-theme="c">
    <h2>@Model.Name</h2>
    <div>
        <div>
            @Model.Description
        </div>
        <div>
            <strong>(@Model.Price.ToString("# руб"))</strong>
        </div>
        <div>
            @using (Html.BeginForm("AddToCart", "Cart"))
            {
                 @Html.HiddenFor(x => x.GameId)
                 @Html.Hidden("returnUrl", Request.Url.PathAndQuery)
                 <input type="submit" class="btn btn-success" value="В корзину" />
            }
        </div>
    </div>
</div>

Внутри этого представления используется виджет jQuery Mobile, который позволяет пользователям разворачивать и сворачивать области содержимого. Такой способ отображения информации о товарах далек от идеала, однако он прост. Результат визуализации этого нового представления можно видеть на рисунке:

Результат визуализации представлений, специфичных для мобильных устройств

Разумеется, в реальном проекте работа была бы продолжена созданием мобильных версий представлений, которые отображают ссылки на страницы, корзину для покупок и форму оплаты. Но в рамках примера все это делаться не будет, поскольку вы уже знаете, каким образом MVC Framework позволяет ориентироваться на мобильные устройства.

Хотите победить давнего врага в World of Warcraft? Вы можете купить золото wow в интернет-магазине, чтобы получить то оружие, которое даст вам преимущество.

Пройди тесты
Лучший чат для C# программистов