ASP.NET MVC 4 Mobile Features устарели быстрее чем появились

    Что такое Mobile Features в ASP.NET MVC 4


    ASP.NET MVC 4 вышел с очень ожидаемой и долгожданной новой возможностью — то, что по английски называют Mobile Features — поддержкой мобильных устройств. По большому счету, название сразу удивило, так как немного расходится с техническим описанием нововведения. Но промоушен — есть промоушен. Все упоминания про шаблоны страниц (layout) с поддержкой HTML 5, указание viewport, CSS media — все это к MVC фреймворку непосредственно отношения не имеет.

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

    И здесь сразу стоит заметить, что слово «мобильный» упоминается исключительно в рекламных целях. MVC 4 позволяет создавать несколько View для каждой из требуемых страниц и определять для какого браузера каждая из них должна показываться. То есть, возможности ни коем образом не ограничиваются именно мобильными браузерами.

    Все гениальное просто — «hook» во ViewEngine


    Реализация данной возможности очень проста. Это просто «hook» во ViewEngine.

    Вы делаете разные страницы (Views) для разных устройств или браузеров. Именуете файлы добавляя ключевое слово:
    • Catalog.cshtml
    • Catalog.iPhone.cshtml

    Задаете критерий по которому будет выбираться нужная View — пользуясь DisplayModeProvider задаете нужные DefaultDisplayMode с нужным именем.

    DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("iPhone")
    {
        ContextCondition = (context => context.GetOverriddenUserAgent().IndexOf
             ("iPhone", StringComparison.OrdinalIgnoreCase) >= 0)
    });
    

    В рантайме, когда отработает метод контроллера, подготовленный ViewResult передается во View Engine, проверяются условия DisplayModeProvider-a, он изменяет имя файла View, которое нужно загрузить.

    Можем ли мы этим реально пользоваться?


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

    Уже настало время, когда веб-сайт должен поддерживать браузеры не только на десктопных компьютерах, ноутбуках и мобильных устройствах, но еще и на телевизорах. Причем к мобильным устройствам относятся обычные телефоны, смартфоны и планшетные компьютеры. И согласитесь — если внешний вид на планшете и смартфоне еще может быть одинаковым, с одними и теми же JavaScript виджетами, то для обычных сотовых телефонов, коих еще много, такое же представление и такие же возможности Web UI обеспечить сложно.

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

    Поддержка телевизоров выглядит очень простой задачей. Full HD означает одинаковый размер экрана в пикселах. Но на этом плюсы заканчиваются.

    Оказывается, если Ваш сайт открыли с телевизора, он мог быть открыт в браузерах на:
    • телевизоре Phillips Smart TV — NetTV SDK — сайт на CE-HTML
    • телевизоре Samsung Smart TV — у Самсунга свое SDK, возможности сильно отличаются от NetTV
    • телевизоре LG Smart TV
    • с приставки XBOX 360 — Internet Explorer
    • с приставки PlayStation 3
    • с приставки Wii
    • с Google TV, телевизоры Sony со встроенным Google TV
    • с Apple TV
    • Boxee box, Kylo browser, Roku и другие

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

    Например, на XBOX работает обычный Internet Explorer, ширина экрана вполне достаточная для работы с обычной десктопной версией сайта, но пользователь вынужден на джойстике возить по экрану курсор. Для того, чтобы нажать на кнопку на экране ему требуется навести на нее курсор и нажать кнопку на джойстике. Становится очевидным, что более подходящий интерфейс — это интерфейс для планшетов, с большими прямоугольными кнопками. Комбобоксы также не удобны. Пролистывать страницу на джойстике достаточно удобно.

    На SmartTV Вам придется расчитывать на то, что нет курсора и пользователь бегает по кнопкам и инпутам на странице нажимая кнопки вверх/вниз/влево/вправо на пульте. Чтобы поддерживать другие кнопки на пульте, которые упрощают навигацию, придется прочитать документацию из SDK и писать JavaScript специфичный для конретного устройства — key codes разные.

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

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

    Что можно предложить взамен?


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

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

    Учитывая архитектуру ASP.NET MVC 4, есть только один подходящий вариант. Внутри MVC приложения (проекта), каждый из вариантов сайта должен быть выполнен в своей Area. По User Agent String мы может определить от какого устройства/браузера поступает запрос и направить его в нужную Area.

    Чтобы переправить в нужную Area нужно для кажого маршрута (Route) в таблице рутинга добавить ограничение (Constraint).

    Минус такого подхода в том, что такое ограничение нужно не забыть добавить во все маршруты.

    Например, если мы сделали Area в которую нужно перенаправлять всех с Chrome browser, то Area Registration будет выглядеть так:

      public class ChromeAreaRegistration : AreaRegistration
      {
        public override string AreaName
        {
          get { return "Chrome"; }
        }
    
        public override void RegisterArea(AreaRegistrationContext context)
        {
          context.MapRoute(
              "Chrome_default",
              "{controller}/{action}/{id}",
              new { controller = "Home", action = "Index", id = UrlParameter.Optional },
              new { isChromeBrowser = new IsChromeConstraint() }
          );
        }
      }
    

    При этом ограничение будет выглядеть так:

      public class IsChromeConstraint : IRouteConstraint
      {
        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
          string userAgent = httpContext.Request.UserAgent;
          if (string.IsNullOrWhiteSpace(userAgent)) { return false; }
    
          return userAgent.Contains("Chrome");
        }
      }
    

    При этом у нас есть основная Area, куда должны направляться все остальные браузеры, кроме Хрома.

      public class DesktopAreaRegistration : AreaRegistration
      {
        public override string AreaName
        {
          get { return "Desktop"; }
        }
    
        public override void RegisterArea(AreaRegistrationContext context)
        {
          context.MapRoute(
              "Desktop_default",
              "{controller}/{action}/{id}",
              new { controller = "Home", action = "Index", id = UrlParameter.Optional },
              new { isNotChromeBrowser = new IsNotChromeConstraint() }
          );
        }
      }
    

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

      public class IsNotChromeConstraint : IRouteConstraint
      {
        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
          string userAgent = httpContext.Request.UserAgent;
          if (string.IsNullOrWhiteSpace(userAgent)) { return false; }
    
          return !userAgent.Contains("Chrome");
        }
      }
    

    Выставлять ограничения для всех маршрутов во всех Area необходимо, несмотря на то, что маршруты в рантайме просматриваются сверху вниз. Дело в том, что порядок регистрации AreaRegistration при старте случайный. Этого можно избежать, если регистрировать маршруты всех Area в одном месте — в Global.asax — явно указывая их порядок. Тогда можно гарантировать, что проверка на Хром браузер всегда будет происходить первой.

    Постскриптум


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

    Ну. И что?
    Реклама
    Комментарии 18
      +1
      Странно, что проблема различных дивайсов решается на стороне сервера, а не клиента, как это принято делать, например, при помощи CSS media queries.
        +1
        очевидна проблема: слабое устройство может не суметь (в силу технических причин) обработать (да еще и верно) CSS «трюки».

        Вы в реальных проектах пробовали это использовать? Я пробовал. Результат резко негативный. Мой «слегка устаревший» телефон не мог прожевать нормально «большой и сложный сайт» — тормозил, показывал некорретно, вылетал из браузера...
        +3
        > Основное неудобство именно в том, что все View, которые вы делаете для страницы обрабатываются
        > одним и тем же методом контроллера.

        А можно реальный случай, когда для разных устройств нужные принципиально разные Controllers?

        В остальном, статья от кэпа. Можно было не мутить кучу воды и после небольшого вступления перейти к IsNotChromeConstraint и по тексту.
          0
          Да, приведу пример на странице показывающей пользователю ассортимент товара — обычный browse list с картинками товаров.

          Оптимальным подходом будет следующее:

          Для XBOX мы показываем товар как обычно, ширина фиксирована, пейджинг нам не нужен — пользователь пролистывает страницу вниз и мы всевремя подгружаем вниз товар для показа (infinite scrolling). Страница может содержать виджеты социальных сетей, кнопки рейтинга товара и тому подобное.

          Для SmartTV — ширина та же, но необходимо избавится от скроллинга, товар показывается постранично, все умещается на страницу без скроллинга, слева и справа страницы кнопки Влево/Вправо, в JavaScript обрабатываем key press events с кнопок пульта и делаем пост-беки для пейджинга. Никаких виджетов.

          То есть и сама страница, ее функционал и внешний вид, а также server side для педжинга будут различными.
            +1
            И какие там различия? Вы не номер страницы передавайте, а количество «сколько надо данных в штуках», «с какого id» и «с какой сортировкой и фильтрами». Магия и чуток умения — и никаких отличий.

            К слову все компоненты (типа DevExpress) умеют такое — в asp.net там пейнджинг ИЛИ виртуальный режим без каких-то изменений данных для них в гридах.
              0
              Я бы сказал что различие в том, что
              — для infinite scrolling у нас есть action method который принимает параметры и отдает JSON
              — а для постраничного пейджинга у нас есть action method который отдает aspx/cshtml View, причем размер страницы в элементах фиксированный и не должен передаваться через параметры — у пытливого пользователя не должно быть возможности этим управлять.

              Передавать параметр в метод управляя тем в каком формате отдавать результат или опираться на content-type запроса в данном случае не верно.

              У Вас может использоватся одна и таже логика пейджинга, но методы контроллера — это presentation layer. Они будут разными и это правильно. Здесь не нужно стремиться делать универсальные механизмы.
                0
                Что-то вы все в кучу смешали. Json метод вообще в стороне тут (не влияет на код контроллера).

                Смотрите: контроллер выдает одну страницу (первую). Далее представление для Xbox показывает страницу + организует скроллинг через JS. А ТВ версия опять лезет к контроллеру за новой страницей.
                  0
                  Я проиллюстрирую то, что предложил:

                  для Smart TV потребуется один метод:
                  — /catalog/{pageId} — возвращает html page

                  для Xbox/IE потребуется два метода:
                  — /api/catalog/{parameters set} — возвращает json для infinite scrolling
                  — /catalog — возвращает html page, нет input параметров

                  Вы предлагаете для обоих случаев пользоваться одним методом — - /catalog/{pageId}

                  Это возможно, но не правильно. Объясню почему.

                  Вы отдаете приложение на QA, они на xbox IE набирают /catalog/2 и выписывают дефект примерно следующего содержания: «как бы дитя не скролилло, любимых медведей не увидело». У Вас два пути решения проблемы:
                  — сделать для xbox метод без параметра
                  — сделать в контроллере условную логику основанную на том какой девайс/браузер используется, что является не верным решением. Именно от этого и следует уходить любыми средствами. Аналогичная условная логика во View также не желательна — поэтому MS и сделали View Modes в MVC 4.
                    0
                    > — /catalog — возвращает html page, нет input параметров

                    default values не? Тогда /catalog эквивалент /catalog/1

                    > они на xbox IE набирают /catalog/2

                    с какой это радости они не увидят результат? они получат вид «страница 2». А затем уже задача JS обеспечить скролинг в обе стороны.
                      0
                      infinite scrolling на JavaScript в обе стороны — интересная тема для блога, не хотите написать топик?
                        +1
                        Не в обе стороны. Вверх он будет вполне конечный.
                  +2
                  Вы не умеете делать asp.net mvc контролы, это я уже понял. Может будете удивлены, но контролы могут отдать и json.
                  Поясняю для тех кто незнаком:
                  1) вы пишете в контроллере нечто вроде
                  ActionResult Index() { var model = GetModel(GridQueryParams.Default); return View(model); } ActionResut MyGridPartial(GridQueryParams p) { var model = GetModel(p); return PartialView(model); }
                  2) в коде контрола пишете нечто вроде
                  @if(MyGridHelper.IsCallback){ Html.Raw(Model.ToJson()); //верните модель в callback-е как json } else{ <!-- это собственно контрол таблица, тут же ссылки вперед-назад, которые обрабатываются javascript-том (вызывая Partial метод, принимая данные, и делая с ними ЧТО угодно, включая "перезаполнение" таблицы). --> <table> blablabla </table> }
                  т.е. при калбэке верните json, иначе (по дефолту) html.
                  3) оборачиваете это всё в класс. Делаете экстеншен для HtmlHelper (так типа принято).
                  4) по вкусу: влияете на генерированный код контрола передавая ему параметры, например route values — что вызвать на callback. Также по вкусу выставляете (чем угодно — хоть пост фильтром) у каллбэк значения content-type и т.д.

                  NOTE: для того чтобы понять что к вам пришел callback вам надо передать нечто (значение) в самом запросе любым методом и обработать это нечто в MyGridHelper.get_IsCallback().

                  UPD: соответсвенно вам просто надо два контрола нарисовать — под «ajax» (как я написал выше) и «не под ajax».
                +1
                Согласен с jonie. Больше подходит для попытки прикрутить к обычному сайту костыль для SmartTV.
                  0
                  Это спор аналогичный тому стоит ли пытаться на HTML 5 делать одно приложение работающее и на iOS, и на Android, и на Windows Phone 8. Ответ на него очень простой — если Вам нужен WoW результат и ресурсы позволяют, то нужно делать для каждой платформы нативное приложение.

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

                  В то же время открывать большенство сайтов на мобильных устройствах, телевизорах и приставках практически бесполезно, т.к. меню разворачивается на mouse over, комбобоксы закрываются до того как вы проскроллите до нужного пункта и автоматически уходит пост-бек, чтобы добраться до кнопки ОК нужно 25 раз нажать на кнопку Вниз, а на link button никак не попасть с джойстика. И все эти сайты не парятся по этому поводу :)

                  Я бы всем посоветовал запарится, потому что другой возможности сейчас как-то выделится из десятков тысяч аналогов — нет.

                  В условиях конкуренции выигрывает тот, кто к лесу задом, а к пользователю передом.
                    +1
                    И где спор? Выше я говорил, что ваш пример — явный костыль. Т.е. изначально надо рассчитывать на поддержку разных платформ. И как раз «один сайт на HTML5» к моим словам не при чем.
              0
              Для реализации IsNotChromeConstraint лучше было бы использовать существующий IsChromeConstraint и отрицание. Добавить конструктор:
              public IsNotChromeConstraint(IsChromeConstraint chromeConstraint)
              {
              _chromeConstraint = chromeConstraint;
              }

              а в вызове метода Match класса IsNotChromeConstraint возвращать:
              return !_chromeConstraint.Match(...);
                0
                Верно :) еще лучше сделать Constraint, который принимает делегат с условием — аналогично тому как задаются DisplayMode в DisplayModeProvider-e.
                0
                Если уж так необходимы разные контроллеры или экшены для разных клиентов, не проще ли сделать свою реализацию ActionInvoker, который будет ориентироваться, например, на некий атрибут в котором указывать для DisplayMode этот экшен?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое