ASP.NET MVC 3

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

Шейн Черч

Продукты и технологии:

ASP.NET MVC 3, Windows Phone

В статье рассматриваются:

  • концепция гибридного приложения;
  • создание веб-приложения;
  • использование jQuery Mobile;
  • оболочки родных мобильных приложений;
  • ориентация на платформы Windows Phone, Android и iOS;
  • корректное сокращение функциональности (graceful degradation).

Исходный код можно скачать по ссылке code.msdn.microsoft.com/mag201203MobileWeb.

Вы хотите создать мобильное приложение, но пребываете в растерянности от изобилия существующих устройств и API, которые надо изучать. Какую мобильную платформу следует предпочесть? Apple iOS (iPhone и iPad) использует Objective C, Google Android — Java, а Windows Phone — Silverlight, и в то же время каждый из этих вариантов имеет свой API и свою рыночную нишу. Решив сосредоточиться только на одном конкретном стеке технологий, вы потеряете 50% рынка (или более) — остальные не смогут пользоваться вашим приложением. Если вы попытаетесь поддерживать все эти платформы, вам придется иметь дело, как минимум, с тремя совершенно разными кодовыми базами, что значительно увеличит ваши затраты на разработку и сопровождение.

Но есть другой вариант: вы могли бы создать мобильное веб-приложение — оно будет работать на любом из этих устройств. Однако у такого подхода есть свои проблемы. Самое большое препятствие для разработки всего бизнес-приложения на HTML и JavaScript — отсутствие доступа ко многим аппаратным средствам мобильных устройств, например к камере, GPS или акселерометру.

Очевидно, рынок мобильных устройств будет развиваться и дальше, а значит, возникает вопрос: как поддерживать все эти аппаратные платформы и обеспечить максимальное удобство использования ваших приложений? В этой статье я покажу, как создать мобильное приложение, способное задействовать преимущества из двух миров, и для этого оберну мобильное веб-приложение в оболочку родных приложений (native application shell).

Концепция гибридного приложения

Базовая концепция гибридного приложения заключается в обертывании веб-приложения, оптимизированного под мобильные устройства, в специфическую для конкретного устройства оболочку родных приложений. Такая оболочка выступает в роли хоста для элемента управления «веб-браузер» (Web browser control), который конфигурируется на запуск мобильного приложения по определенному URL при запуске программы оболочки. (В родной оболочке при необходимости могут быть предоставлены и другие UI-элементы, но обязательным является лишь элемент управления «веб-браузер».) Далее по мере навигации пользователя по сайту родной элемент управления «веб-браузер» прослушивает запрашиваемые URL. Когда пользователь запрашивает определенный URL, требующий задействовать аппаратные функции данного устройства, этот элемент управления прерывает событие навигации и вместо него запускает аппаратные функции. Когда пользователь завершает родной процесс, приложение указывает элементу управления «веб-браузер» вернуться в соответствующее место веб-сайта.

Чтобы проиллюстрировать, как это делается, я подробно расскажу вам, как я — вместе с коллегами из EffectiveUI — создавал приложение для одного клиента. Это приложение предназначено для мобильных сотрудников, обрабатывающих заказы на техническое обслуживание и ремонт различного муниципального имущества, такого как рекламные щиты, скамейки и пожарные гидранты. Для определения текущего местонахождения сотрудника оно использует преимущества функций, поддерживаемых браузером, а для получения снимков муниципального имущества и их последующей загрузки на сервер — доступ к аппаратным средствам конкретного устройства. Основное меню этого приложения показано на рис. 1.

The Completed Application Main Menu
Рис. 1. Основное меню законченного приложения

Создание веб-приложения

При создании этого мобильного приложения я последовал ряду предложений, высказанных в статье Стива Сендерсона (Steve Sanderson) «Build a Better Mobile Browsing Experience» (msdn.microsoft.com/magazine/hh288079) в номере «MSDN Magazine» за июль 2011 г. В дополнение к рекомендациям из этой статьи я дам на основе своего опыта еще несколько советов.

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

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

  • Оптимизируйте мобильные представления для экономии полосы пропускания Ресурсы большинства мобильных устройств ограниченны, особенно это касается полосы пропускания. Не заставляйте пользователей скачивать наборы крупных изображений, чтобы использовать ваш сайт. Пользователи мобильных устройств ожидают, что интерфейсы будут реагировать быстро, и, если ваш сайт или приложение не будут работать в соответствии с их ожиданиями, откажутся от вашего сайта или приложения.
  • Используйте HTML5 и CSS3 Поскольку мобильные веб-браузеры — в отличие от настольных — не отягощены наследием давнего прошлого, в них гораздо быстрее внедряются появляющиеся стандарты HTML5 и CSS3. Во многих случаях мобильные браузеры намного опережают настольные в реализации функциональности этих стандартов. Используйте это преимущество в своих мобильных представлениях для уменьшения объема данных, которые приходится скачивать мобильным браузерам, и отдавайте на откуп этим браузерам больше функций рендеринга и применения стилей.

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

Рис. 2. Вспомогательная функция, определяющая, какое представление следует выводить

private ActionResult SelectView(string viewName, object model,
  string outputType = "html")
{
  if (outputType.ToLower() == "json")
  {
    return Json(model, JsonRequestBehavior.AllowGet);
  }
  else
  {
    #if MOBILE
      return View(viewName + "Mobile", model);
    #else
      if (Request.Browser.IsMobileDevice)
      {
        return View(viewName + "Mobile", model);
      }
      else
      {
        return View(viewName, model);
      }
    #endif
  }
}

Эта функция позволила мне соблюсти требование использовать один и тот же код для принятия решения о том, какое представление следует передавать пользователю на основе входящего запроса. Если входящий запрос является скриптом, который запрашивает JSON вместо HTML, контроллер отреагирует, установив соответствующее значение для параметра outputType. Я также использую выражение препроцессора для проверки символа условной компиляции MOBILE, включающего отладку мобильных представлений с применением настольных браузеров. Значение этого символа устанавливается с применением дополнительной мишени компиляции «Mobile» в проекте ASP.NET MVC 3, и это позволяет мне пропускать проверку для Request.Browser.IsMobileDevice в настольной отладочной конфигурации, что значительно ускоряет создание и отладку мобильной версии приложения.

Создавая приложение, я также использовал раздельные эталонные страницы (master pages) для мобильной и настольной версий сайта. Настольная и мобильная версии эталонной страницы сильно различаются из-за необходимости учитывать расхождения в отображении на этих платформах. Мобильная версия эталонной страницы включает специфичные для мобильного устройства CSS-файлы и упрощенную структуру разметки, что облегчает разработку индивидуальных представлений с применением разметки и синтаксиса jQuery Mobile Framework.

Все современные мобильные платформы обеспечивают доступ к GPS-модулю устройства для определения текущего местоположения пользователя через API геопозиционирования, соответствующий W3C-спецификации HTML5. Как использовать этот API, подробно обсуждалось в статье Брэндона Сэтрома (Brandon Satrom) «Integrating Geolocation into Web Applications» (msdn.microsoft.com/magazine/hh580735) в декабрьском номере за 2011 г. Хотя в той статье говорилось об использовании поли-заполнений JavaScript для HTML5 с целью поддержки геопозиционирования в браузерах, которые не поддерживают HTML5 API геопозиционирования, большинство нынешних мобильных браузеров изначально поддерживает этот API, поэтому методика поли-заполнений вам скорее всего не потребуется. Оценивая необходимость использования методики поли-заполнений, вы должны принять во внимание возможности устройств и браузеров, на которые вы ориентируетесь. В случае Android следует учесть, что параметр enableHighAccuracy должен быть установлен в true при вызовах API геопозиционирования для успешного доступа к функциональности GPS в эмуляторе Android, как показано на рис. 3.

Рис. 3. Геопозиционирование через HTML5

if(navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
$("#map_canvas").GoogleMap("addMarker", {
id: "device_location",
latitude: position.coords.latitude,
longitude: position.coords.longitude,
description: "Current Location",
iconImageUrl: '@Url.Content("~/Content/images/my-location-dot.png")',
callback: function () {
$("#map_canvas").GoogleMap("panToLocation", {
latitude: position.coords.latitude,
longitude: position.coords.longitude
});
}
});
}, function (error) {
}, {
enableHighAccuracy: true
});
}

Использование jQuery Mobile

Инфраструктура jQuery Mobile Framework — это «унифицированная система UI на основе HTML5 для всех популярных мобильных платформ», как сказано на веб-сайте этого проекта (jquerymobile.com). Он содержит ряд виджетов, оптимизированных под сенсорное взаимодействие, и значительно упрощает создание мобильных веб-приложений, интерфейсы которых по своему духу и букве полностью соответствуют родным приложениям мобильных устройств. jQuery Mobile можно добавить в ваш проект ASP.NET MVC 3 через NuGet, используя интерфейсNuGet Package Manager, или из Package Manager Console, выполнив команду «Install-Package jquery.mobile» (без кавычек). Эта команда добавит в ваш проект файлы jQuery Mobile JavaScript и CSS. Однако вам все равно потребуется добавить ссылки на файлы jQuery Mobile JavaScript и CSS в мобильную версию эталонной страницы, как показано на рис. 4.

Рис. 4. Мобильная версия эталонной страницы

<!DOCTYPE html>
<html>
<head>
  <title>@ViewBag.Title</title>
  <meta name="viewport" content="width=device-width,
    initial-scale=1.0, user-scalable=no, height=device-height" />
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
  <link href="@Url.Content("~/Content/eui_assets/css/reset.css")"
    rel="stylesheet" type="text/css" />
  <link href="@Url.Content("~/Content/jquery.mobile-1.0.min.css")"
    rel="stylesheet" type="text/css" />
  <link href="@Url.Content("~/Content/mobile.css")"
    rel="stylesheet" type="text/css" />
  <script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")"
    type="text/javascript"></script>
  @RenderSection("PreJQueryMobileInit", false)
  <script src="@Url.Content("~/Scripts/jquery.mobile-1.0.min.js")" 
    type="text/javascript"></script>
  <script type="text/javascript">
    $('a[data-ajax="false"]').live('click', function (event) {
      if (!$(this).hasClass("camera-link")) {
        $.mobile.showPageLoadingMsg();
      }
    });
  </script>
  @RenderSection("Head", false)
</head>
<body class="eui_body" id="@ViewBag.BodyID">
  @RenderBody()
</body>
</html>

jQuery Mobile вносит некоторые существенные изменения в шаблоны, привычные любому разработчику на jQuery. Процитирую документацию на jQuery Mobile:

Первое, чему вы учитесь вjQuery, — вызывать код внутри функции $(document).ready(), чтобы все выполнялось, как только загружаетсяDOM. Однако вjQueryMobile для загрузки контента каждой страницы вDOM в процессе навигации используется [AJAX], и обработчик готовностиDOM (readyhandler) выполняется только для первой страницы. Чтобы выполнять код всякий раз, когда загружается и создается новая страница, вы можете подключиться к событиюpageinit.

Я использовал событие pageinit во всех страницах приложения, которые содержали элемент управления Google Maps, чтобы инициализировать карту, когда страница преобразуется в представление через AJAX.

Дополнительная функциональность мобильной версии эталонной страницы — строка @RenderSection("PreJQueryMobileInit", false), приведенная на рис. 4, которая позволяет выполнять скрипты до инициализации jQuery Mobile в данной странице. В этом приложении-примере я задействовал данную функциональность для подключения события mobileinit, чтобы установить собственный обратный вызов по окончании работы фильтра listview в jQuery Mobile. Я также добавит две строки кода в библиотеку jQuery Mobile, чтобы включить метод filterCompleteCallback в прототип listview для получения уведомления о завершении фильтрации встроенного списка (built-in list filtering). Это позволило обновлять элементы на карте, соответствующие элементам отфильтрованного списка. Функцию обратного вызова нужно добавлять в jQuery Mobile listview до применения jQuery Mobile к любой разметке; этот код выполняется в обработчике события mobileinit, показанном на рис. 5.

Рис. 5. Подключение к событию mobileinit

if(navigator.geolocation) {   
  navigator.geolocation.getCurrentPosition(function (position) {
    $("#map_canvas").GoogleMap("addMarker", {
      id: "device_location",
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      description: "Current Location",
      iconImageUrl: '@Url.Content("~/Content/images/my-location-dot.png")',
      callback: function () {
        $("#map_canvas").GoogleMap("panToLocation", {
          latitude: position.coords.latitude,
          longitude: position.coords.longitude
        });
      }
    });
  }, function (error) {
  }, {
    enableHighAccuracy: true
  });
}
@section PreJQueryMobileInit {
  <script type="text/javascript">
    $(document).bind("mobileinit", function () {
      $.mobile.listview.prototype.options.filterCompleteCallback = function () {
        // Note that filtercompletecallback is a custom
        // addition to jQuery Mobile and would need to be updated
        // in future revisions.
        // See comments in jquery.mobile-1.0.js with SSC 09/12/2011
        var ids = [];
        var $visibleItems = $("#js-work-orders-list").find(
          "li:not(.ui-screen-hidden");
        for (var i = 0; i < $visibleItems.length; i++) {
          var item = $($visibleItems[i]).find("p");
          ids.push(item.text().substr(item.text().indexOf('#') + 1));
        }
        ids.push("device_location");
        $("#map_canvas").GoogleMap("hideAllMarkersExceptList", ids);
      }
    });
  </script>
}

jQuery Mobile интенсивно использует новые средства HTML5, такие как теги header и footer и атрибуты data-*. Атрибуты data-role определяют поведение, которое нужно подключить к данному элементу. Например, в представлении MapMobile.cshtml на рис. 6 у меня есть два тега div, определенных с атрибутом data-role="page".

Рис. 6. Разметка MapMobile.cshtml

<div data-role="page" id="map_page" data-fullscreen="true"
  data-url="map_page" data-theme="a">
  <header data-role="header" data-position="fixed">
    <a href="@Url.Action("Index", "Home")" data-icon="home"
      data-direction="reverse">Home</a>
    <h1>Map Demo</h1>
    <a href="#" data-icon="back" id="js-exit-street-view"
      class="ui-btn-hidden">Exit Street View</a>
  </header>
  <div data-role="content" class="main-content">
    <div id="map_canvas" style="width:100%;height:100%"></div>
  </div>
  <footer data-role="footer" data-position="fixed"
    data-id="fixed-nav" data-theme="a">
    <nav data-role="navbar">
      <ul>
        <li><a href="#map_page" class="ui-btn-active
          ui-state-persist">Map</a></li>
        <li><a href="#items_page">Work Orders</a></li>
      </ul>
    </nav>
  </footer>
</div>
<div data-role="page" id="items_page" data-url="items_page" data-theme="a">
  <header data-role="header" data-position="fixed">
    <a href="@Url.Action("Index", "Home")" data-icon="home"
      data-direction="reverse">Home</a>
    <h1>Map Demo</h1>
  </header>
  <div data-role="content" class="main-content">
    <div class="list-container">
      <ul data-role="listview" id="js-work-orders-list" data-filter="true">
      @foreach (MapItem item in Model.Items)
  {
      <li class="work-order-id-@item.ID">
        <a href="@Url.Action("Details", "Home", new { id = item.ID })"
          data-ajax="false">
          <h3>@item.Issue</h3>
          <p>Work Order #@item.ID</p>
        </a>
      </li>
    }
      </ul>
    </div>
  </div>
  <footer data-role="footer" data-position="fixed"
    data-id="fixed-nav" data-theme="a">
    <nav data-role="navbar">
      <ul>
        <li><a href="#map_page" data-direction="reverse">Map</a></li>
        <li><a href="#items_page" class="ui-btn-active
          ui-state-persist">Work Orders</a></li>
      </ul>
    </nav>
  </footer>
</div>

Этот атрибут сообщает jQuery Mobile, что каждый из этих div должен интерпретироваться как отдельная страница на мобильном устройстве и что переход между ними должен осуществляться с помощью AJAX без навигации по страницам, происходящей в браузере. Это создает эффект, продемонстрированный на экранных снимках на рис. 7. Другие рекомендации и более подробное описание того, как использовать каждый из атрибутов data-* в контексте jQuery Mobile, вы найдете на веб-сайте jQuery Mobile.

Transitioning Between Pages Via AJAX
Рис. 7. Переход между страницами через AJAX

Создание оболочки родных мобильных приложений

Базовый шаблон в разработке каждой оболочки родных приложений заключается в проектировании приложения, которое просто содержит полноэкранный элемент управления «веб-браузер». Внутри этого элемента я захватываю событие, срабатывающее, когда пользователь запрашивает новую страницу, и сравниваю запрошенный URL со списком известных URL, которые должны запускать аппаратную («родную») функциональность. Именно здесь творится вся магия веб-приложения в оболочке родных приложений. В своем приложении я сравниваю URL с адресом внутри моего сайта для «Home/Image», чтобы задействовать функциональность встроенной камеры. Когда пользователь попадает на страницу подробных сведений Work Order, он видит значок камеры в правом верхнем углу экрана, как показано на рис. 8. Щелчок этой кнопки запускает встроенную камеру.

Invoking Native Camera Functionality
Рис. 8. Запуск встроенной камеры

Windows Phone

Windows Phone для работы со всей встроенной функциональностью использует Silverlight. В некоторых отношения это делает Windows Phone самой удобной платформой для поддержки мобильных веб-приложений, если разработчик знаком с ASP.NET. Базовая XAML-разметка для оболочки родных приложений весьма проста:

<Canvas x:Name="LayoutRoot" Background="Black" Margin="0">
  <phone:WebBrowser HorizontalAlignment="Left" Name="webBrowser1" 
    Navigating="webBrowser1_Navigating" IsScriptEnabled="True"
    IsGeolocationEnabled="True"
    Background="Black" Height="720" Width="480" />
</Canvas>

Здесь следует отметить, что IsScriptEnabled устанавливается в true, потому что по умолчанию элемент управления «веб-браузер» в Windows Phone не поддерживает скрипт; кроме того, я обрабатываю событие Navigating.

В MainPage.xaml.cs, показанном на рис. 9, я обрабатываю событие webBrowser1_Navigating. Если URL навигации совпадает с искомым URL, выбирается идентификатор подряда на работы, с которым я работаю, и вызывается родная функция CameraCaptureTask с одновременной отменой навигации в веб-браузере. После того как пользователь делает снимок с помощью камеры, вызывается метод
photoCaptureOrSelectionCompleted. В нем я загружаю снимок на веб-сервер, используя ту же операцию POST HTTP-формы, которую задействовал бы веб-сайт, если бы я передавал форму, где есть кнопку загрузки файла на сервер. По завершении загрузки снимка вызывается upload_FormUploadCompleted, и пользователь возвращается в ту же точку веб-приложения.

Рис. 9. MainPage.xaml.cs для Windows Phone

public partial class MainPage : PhoneApplicationPage
{
  CameraCaptureTask cameraCaptureTask;
  BitmapImage bmp;
  string id = "";
  string baseURL = "http://...";
  // Constructor
  public MainPage()
  {
    InitializeComponent();
    cameraCaptureTask = new CameraCaptureTask();
    cameraCaptureTask.Completed +=
      new EventHandler<PhotoResult>(photoCaptureOrSelectionCompleted);
  }
  private void webBrowser1_Navigating(object sender, NavigatingEventArgs e)
  {
    // Catch Navigation and launch local camera
    if (e.Uri.AbsoluteUri.ToLower().Contains("home/image"))
    {
      id = e.Uri.AbsoluteUri.Substring(e.Uri.AbsoluteUri.LastIndexOf("/") + 1);
      cameraCaptureTask.Show();
      e.Cancel = true;
    }
  }
  void photoCaptureOrSelectionCompleted(object sender, PhotoResult e)
  {
    if (e.TaskResult == TaskResult.OK)
    {
      byte[] data = new byte[e.ChosenPhoto.Length];
      e.ChosenPhoto.Read(data, 0, data.Length);
      e.ChosenPhoto.Close();
      Guid fileId = Guid.NewGuid();
      Dictionary<string, object> postParameters = new Dictionary<string, object>();
      postParameters.Add("photo", new FormUpload.FileParameter(
        data, fileId.ToString() +
        ".png", "image/jpeg"));
      FormUpload upload =
        new FormUpload(baseURL + "Home/UploadPicture/" + id, postParameters);
      upload.FormUploadCompleted +=
        new FormUpload.FormUploadCompletedHandler(upload_FormUploadCompleted);
      upload.BeginMultipartFormDataPost();
    }
  }
  void upload_FormUploadCompleted(object source)
  {
    webBrowser1.Navigate(webBrowser1.Source);
  }
  private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
  {
    webBrowser1.Navigate(new Uri(baseURL));
  }
}

Windows Phone ведет себя несколько иначе при взаимодействии с веб-версией элемента управления Google Maps или Bing Maps по сравнению с Android или iOS. Из-за того, что мобильный браузер Internet Explorer 9 захватывает жесты касания, смещения и разведения без передачи их через ядро JavaScript, веб-карты нельзя масштабировать или панорамировать с помощью жестов — нужно использовать элементы управления масштабирования и панорамирования, предоставляемые самой картой. Учитывая это ограничение, было бы неплохо, если бы данный проект усовершенствовали для запуска родного элемента управления Bing Maps в Windows Phone, когда требуется функциональность интерактивной карты, и последующего возврата в веб-приложение на экранах, где такая функциональность не нужна.

Android

Java-код для Android был написан моим коллегой, Шоном Кристманном (Sean Christmann), и он аналогичен коду для Windows Phone. Следующий XML-код определяет полноэкранную разметку для элемента управления WebView в Android:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <WebView android:id="@+id/webView"
      android:layout_width="match_parent" 
      android:layout_height="match_parent"></WebView>
</LinearLayout>

В файле EffectiveUIActivity.java, показанном на рис. 10, переопределенная версия onCreate настраивает WebViewClient на переопределение методов onLoadResource и shouldOverrideUrlLoading элемента управления WebView, чтобы искать ту же совпадающую строку, как это делалось в Windows Phone, и, если таковая найдена, активизировать камеру и отменять навигацию. Этот код также переопределяет onGeolocationPermissionsShowPrompt, чтобы подавлять приглашение пользователю, появляющееся каждый раз, когда приложение выдает разрешение элементу управления WebView на доступ к функциональности геопозиционирования GPS. По завершении операций с камерой функция onActivityResult передает снимок на веб-сервер, используя тот же метод, что и в предыдущем примере с Windows Phone, а затем возвращает пользователя в веб-приложение.

Рис. 10. EffectiveUIActivity.java для Android

public class EffectiveUIActivity extends Activity {
  /** Called when the activity is first created. */
  WebView webView;
  String cameraId;
  static String baseURL = "http://...";
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    webView = (WebView)findViewById(R.id.webView);
    webView.getSettings().setJavaScriptEnabled(true);
    webView.getSettings().setGeolocationEnabled(true);
    webView.setVerticalScrollbarOverlay(true);
    webView.loadUrl(baseURL);
    final EffectiveUIActivity activity = this;
    webView.setWebViewClient(new WebViewClient(){
      @Override
      public void onLoadResource(WebView view, String url) {
        super.onLoadResource(view, url);
        if(url.contains("Home/Image")){
          activity.createCamera();
        }
      }
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url){
        String match = "Home/Image/";
        int i = url.indexOf(match);
        if(i>0){
          cameraId = url.substring(i+match.length());
          activity.createCamera();
          return true;
        }
        return false;
      }
    });
    webView.setWebChromeClient(new WebChromeClient(){
      @Override
      public void onGeolocationPermissionsShowPrompt(
        String origin, GeolocationPermissions.Callback callback) {
        super.onGeolocationPermissionsShowPrompt(origin, callback);
        callback.invoke(origin, true, false);
      }
    });       
  }
  public void createCamera(){
    Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
    startActivityForResult(intent, 2000);
  }
  @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
      if (resultCode == Activity.RESULT_OK && requestCode == 2000) {
        Bitmap thumbnail = (Bitmap) data.getExtras().get("data");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        thumbnail.compress(CompressFormat.JPEG, 75, bos);
        byte[] imagebytes = bos.toByteArray();
        ByteArrayBody bab = new ByteArrayBody(imagebytes, "image/jpeg",
          UUID.nameUUIDFromBytes(imagebytes).toString()+".png");
        HttpClient client = new DefaultHttpClient();
        HttpPost post = new HttpPost(baseURL+"Home/UploadPicture");
        MultipartEntity reqEntity =
          new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
        reqEntity.addPart("photo", bab);
        try {
          reqEntity.addPart("ID", new StringBody(cameraId, "text/plain",
            Charset.forName( "UTF-8" )));
        } catch (UnsupportedEncodingException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
          }
          post.setEntity(reqEntity);
          try {
            HttpResponse response = client.execute(post);
            BufferedReader reader = new BufferedReader(
              new InputStreamReader(
              response.getEntity().getContent(), "UTF-8"));
            String sResponse;
            StringBuilder s = new StringBuilder();
            while ((sResponse = reader.readLine()) != null) {
              s = s.append(sResponse);
            }
          } catch (ClientProtocolException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            }
            webView.loadUrl(webView.getUrl());
          }
        }
}

iOS

Код на Objective-C для iOS также был написан моим коллегой, Шоном Кристманном, и он тоже аналогичен коду, использовавшемуся для Windows Phone и Android. В файле WebCameraViewController.m, показанном на рис. 11, элемент управления UIWebView выполняет метод shouldStartLoadWithRequest, чтобы сравнить запрошенный URL с шаблоном. Если строка URL совпадает с шаблоном, код возвращает «NO» для отмены навигации и вызывает родной метод UIImagePickerController. Это дает возможность пользователю выбрать какое-нибудь изображение из библиотеки снимков или сделать новый снимок с помощью встроенной камеры. После выбора код отправляет снимок на веб-сервер, используя библиотеку ASIFormDataRequest (allseeing-i.com/ASIHTTPRequest) до возврата UIWebView в обычное «русло» приложения.

Рис. 11. Код для iOS

- (void) choosefromCamera {
    UIImagePickerController *picker = [[UIImagePickerController alloc] init];
    picker.delegate = self;
    picker.mediaTypes = [NSArray arrayWithObjects:(NSString*)kUTTypeImage, nil];
    if ([UIImagePickerController
      isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
      picker.sourceType = UIImagePickerControllerSourceTypeCamera;
    }else{
      picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    }
    [self presentModalViewController:picker animated:YES];
}
- (void)imagePickerController:(UIImagePickerController *)picker   
    didFinishPickingMediaWithInfo:(NSDictionary *)info {
    UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage];
    NSData .png = UIImageJPEGRepresentation(image, 0.3);
    [picker dismissModalViewControllerAnimated:YES];
    [picker release];
    NSString *url =
      [NSString stringWithFormat:@"%@:7511/WorkOrders/UploadPicture", baseURL];
    ASIFormDataRequest *request =
      [ASIFormDataRequest requestWithURL:[NSURL URLWithString:url]];
    [request addData.png withFileName:[
      NSString stringWithFormat:@"%@.png", [self GetUUID]]
      andContentType:@"image/jpeg" forKey:@"photo"];
    [request addPostValue:cameraId forKey:@"ID"];
    [request setDelegate:self];
    [request setDidFinishSelector:@selector(imageUploaded:)];
    [request setDidFailSelector:@selector(imageUploaded:)];
    [request startSynchronous];
    [webView reload];
}
-(void) imageUploaded:(ASIFormDataRequest *)request {
    NSString *response = [request responseString];
    NSLog(@"%@",response);
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(
  NSURLRequest *)request
    navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    NSString *str = [url absoluteString];
    NSRange range = [str rangeOfString:@"WorkOrders/Image/"];
    if (range.location != NSNotFound) {
      cameraId = [str substringFromIndex:range.location+17];
      [cameraId retain];
      NSLog(@"%@", cameraId);
      [self choosefromCamera];       return NO;
    }else{
      return YES;
    }
}

Корректное сокращение функциональности для мобильных устройств

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

Чтобы корректно сокращать функциональность, я создал контроллер и представление ASP.NET MVC 3 для URL захвата изображения — «Home/Image», — которое было получено через оболочку родных приложений, и предоставляю простую форму для загрузки файла на сервер (рис. 12). Эта форма позволяет тем, кто не использует расширенные мобильные оболочки, выполнять ту же задачу добавления снимка в подряд на работы, хотя в этом случае интеграция отсутствует. Форма отправляет изображение той же операции контроллера, что и в случае применения оболочек родных приложений, и это способствует повторному использованию кода на разных платформах.

A Simple File Upload Form for Graceful Degradation
Рис. 12. Простая форма загрузки на сервер файла при корректном сокращении функциональности

Гибридное приложение может обеспечить существенную экономию расходов по сравнению с уникальными родными приложениями.

Значительная экономия

Гибридное приложение может обеспечить существенную экономию расходов по сравнению с уникальными родными приложениями — как в краткосрочной перспективе, так и в долгосрочной. Такие средства, как jQuery Mobile, сокращают различия в удобстве использования, что может дать значительные бизнес-преимущества, когда доступ к встроенным аппаратным средствам мобильных устройств не требуется.

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

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

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

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

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


Шейн Черч (Shane Church) — директор EffectiveUI по технологиям (Денвер, штат Колорадо). Занимается разработками в Microsoft .NET Framework с упором на ASP.NET и мобильные технологии Microsoft с 2002 г. Его блог находится по ссылке s-church.net. Узнать больше об EffectiveUI можно на сайте effectiveui.com.

Выражаю благодарность за рецензирование статьи эксперту*** Джеймсу Маккафри (Dr.James McCaffrey).***