Итак, цель – написать на HTML5 мобильное приложения для сбора заказов торговым агентом в торговых точках. Я сталкивался с данными решениями разных компаний, поэтому знаком с предметной областью, и эта тема идеально подходит для мечты.
К основным требованиям я добавлю несколько заметок из собственного опыта:
- Программа должна работать на многих устройствах и на разных платформах. Обычно у компаний, особенно больших, уже есть парк мобильный устройств. Некоторые компании-дистрибьюторы даже заставляют использовать собственные телефоны (так сказать добровольно принудительный BYOD).
- Поддержка офлайн работы. К сожалению интернет покрытие оставляет желать лучшего. Нативные решения хорошо справляются с данной проблемой.
- Программа должна легко расширяться. Почему-то у поставщиков таких решений возникает проблема нормального обновления версий
- Использование железа ( камера, GPS).
Маленькая заметка: Статья написана с целью закрепления пройденного материала по изучению новой технологии. В связи с полным отсутствием реального опыта создания приложений такого рода, заранее прошу прощения за возможные огрехи.
Предварительная архитектура:
Backend — .net MVC with OData. Глобально не важно, что я буду использовать в этой роли, главное, чтобы соответствовало новым стандартам WEB API. Frontend – тут все сложно для меня. При отсутствии опыта выбрать что-то очень сложно. После некоторого просматривания остановился на PhoneJS. Меня подкупило то, что это полноценный фреймворк для SPA приложения, так что не требуется связывать насколько библиотек в кучу, а также использование knockoutjs. Для работы с данными решил использовать breeze. Уверен, что список будет меняться в процессе разработки. Все это потом запаковать при помощи PhoneGap и получить подобие приложения.
В этой статье построим что-то простенькое для начала: просмотр данных торговой точки на определённом маршруте торгового агента.
Создание проекта.
Создаем новый проект ASP.NET MVC 4 Web Application и назовем «MSales». В диалоге New ASP.NET MVC 4 Project выбираем шаблон Web API.
Обновляем пакеты: Update-Package knockoutjs и Update-Package jQuery
, и устанавливаем: Install-Package Breeze.WebApi и Install-Package datajs
.
К сожалению, для PhoneJS нет пакета, поэтому ручками добавляем все необходимые css и js в проект. На выбор есть нескольто типов layout, я использовал NavbarLayout, поменяв файл _Layout.cshtml:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> @Styles.Render("~/Content/css") @Styles.Render("~/Content/dx") @Styles.Render("~/Content/layouts") @Scripts.Render("~/bundles/modernizr") </head> <body> @Html.Partial("NavbarLayout") @RenderBody() @Scripts.Render("~/bundles/jquery") @RenderSection("scripts", required: false) </body> </html>
В файле BundleConfig прописываем весь контент и скрипты. У меня получилось вот так:
// Сокращено для упрощения bundles.Add(new ScriptBundle("~/bundles/knockout").Include( "~/Scripts/knockout-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/breeze").Include( "~/Scripts/q.js", "~/Scripts/datajs-{version}.js", "~/Scripts/breeze.debug.js" )); bundles.Add(new ScriptBundle("~/bundles/dx").Include( "~/Scripts/dx.phonejs.js", "~/Scripts/globalize" )); bundles.Add(new ScriptBundle("~/bundles/app").Include( "~/Scripts/App/app.init.js", "~/Scripts/App/app.viewmodel.js", "~/Scripts/App/NavbarLayout.js" )); bundles.Add(new StyleBundle("~/Content/dx").Include("~/Content/dx/dx.*")); bundles.Add(new StyleBundle("~/Content/layouts").Include("~/Content/layouts/NavbarLayout.css"));
Модель и контролеры
В модель на данный момент включим два файла: классы для маршрутов (по этим маршрутам ходит торговый агент) и торговых точек (магазинов):
public class Route { public int RouteID { get; set; } [Required] [StringLength(30)] public string RouteName { get; set; } } public class Customer { public int CustomerID { get; set; } [Required] [StringLength(50)] public string CustomerName { get; set; } [StringLength(150)] public string Address { get; set; } public string Comment { get; set; } [ForeignKey("Route")] public int RouteID { get; set; } virtual public Route Route { get; set; } }
Контроллеры будут очень простые (более детально про OData можно почитать тут ):
public class RoutesController : EntitySetController<Route, int> { private MSalesContext db = new MSalesContext(); public override IQueryable<Route> Get() { return db.Routes; ; } protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); } } public class CustomersController : EntitySetController<Customer, int> { private MSalesContext db = new MSalesContext(); public override IQueryable<Customer> Get() { return db.Customers; ; } protected override Customer GetEntityByKey(int key) { return db.Customers.FirstOrDefault(p => p.CustomerID == key); } protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); } }
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Routes.MapODataRoute("odata", "odata", GetEdmModel()); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.EnableQuerySupport(); config.EnableSystemDiagnosticsTracing(); } public static IEdmModel GetEdmModel() { ODataModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Route>("Routes"); builder.EntitySet<Customer>("Customers"); builder.Namespace = "MSales.Models"; return builder.GetEdmModel(); } }
При регистрации маршрута для протокола OData необходимо указать строку builder.Namespace = "MSales.Models";
, необходимую для работы библиотек breeze и datajs.
Frontend.
В папке Scripts/app создадим файл скрипта app.init.js для инициализации библиотек:
window.MyApp = {}; $(function () { MyApp.app = new DevExpress.framework.html.HtmlApplication({ namespace: MyApp, defaultLayout: "navbar", navigation: [ { title: "Routes", action: "#route", icon: "home" }, { title: "About", action: "#about", icon: "info" } ] }); MyApp.app.router.register(":view/:id", { view: "route", id: 0 }); MyApp.app.navigate(); var serverAddress = "/odata/"; breeze.config.initializeAdapterInstances({ dataService: "OData" }); MyApp.manager = new breeze.EntityManager(serverAddress); });
Создаем HTML приложение, в котором указываем layout и параметры навигации, которая состоит из двух пунктов: маршруты и about; а также инициализируем библиотеку breeze.
В файле Index.cshtml необходимо разместить dxView и специальную область с именем “content”, в котором выводится обычный список:
<div data-options="dxView : { name: 'route', title: 'Routes' } " > <div class="route-view" data-options="dxContent : { targetPlaceholder: 'content' } " > <div data-bind="dxList: { dataSource: dataSource }"> <div data-options="dxTemplate : { name: 'item' }" data-bind="text: RouteName, dxAction: '#customers/{RouteID}'"/> </div> </div> </div>
Для того, чтобы эти пару строк заработали необходимо, создать Viewmodel, поэтому в папке Scripts/app создадим файл app.viewmodel.js:
MyApp.route = function (params) { var viewModel = { dataSource: { load: function (loadOptions) { if (loadOptions.refresh) { var deferred = new $.Deferred(); var query = breeze.EntityQuery.from("Routes").orderBy("RouteID"); MyApp.manager.executeQuery(query, function (result) { deferred.resolve(result.results); }); return deferred; } } } } return viewModel; };
Хочу обратить внимание что имя Viewmodel совпадает с именем dxView, и содержит только объект dataSource, в которой мы определяем один метод load для загрузки данных. Параметр refresh определяет должны ли данные виджета обновлены полностью. В методе строим запрос, сортируя по полю RouteID и выполняем его.
Добавим еще одну View – About:
<div data-options="dxView : { name: 'about', title: 'About' } "> <div data-options="dxContent : { targetPlaceholder: 'content' } "> <div data-bind="dxScrollView: {}"> <p style="padding: 5px">This is my first SPA application.</p> </div> </div> </div>
Результат для IPhone:
Вы, наверно, обратили внимание, что на элемент списка повешено событие dxAction: '#customers/{RouteID}'
, где, согласно заданной навигации, '#customers
– это вызываемое View, а RouteID
– параметр, передаваемый в это View:
<div data-options="dxView : { name: 'customers', title: 'Customers' } " > <div data-bind="dxCommand: { title: 'Search', placeholder: 'Search...', location: 'create', icon: 'find', action: find }" ></div> <div data-options="dxContent : { targetPlaceholder: 'content' } " > <div data-bind="dxTextbox: { mode: 'search', value: searchString, visible: showSearch, valueUpdateEvent: 'search change keyup' }"></div> <div data-bind="dxList: { dataSource: dataSource }"> <div data-options="dxTemplate : { name: 'item' } " data-bind="text: name, dxAction: '#customer-details/{id}'"/> </div> </div> </div>
В связи с тем, что покупателей может быть много, добавил возможность поиска: добавил dxCommand — кнопка поиска, которая вызывает функцию find, и поле ввода перед списком.
Viewmodel:
MyApp.customers = function (params) { var skip = 0; var PAGE_SIZE = 10; var viewModel = { routeId: params.id, searchString: ko.observable(''), showSearch: ko.observable(false), find: function () { viewModel.showSearch(!viewModel.showSearch()); viewModel.searchString(''); }, dataSource: { changed: new $.Callbacks(), load: function (loadOptions) { if (loadOptions.refresh) { skip = 0; } var deferred = new $.Deferred(); var query = breeze.EntityQuery.from("Customers") .where("CustomerName", "substringof", viewModel.searchString()) .where("RouteID", "eq", viewModel.routeId) .skip(skip) .take(PAGE_SIZE) .orderBy("CustomerID"); MyApp.manager.executeQuery(query, function (result) { skip += PAGE_SIZE; console.log(result); var mapped = $.map(result.results, function (data) { return { name: data.CustomerName, id: data.CustomerID } }); deferred.resolve(mapped); }); return deferred; } } }; ko.computed(function () { return viewModel.searchString(); }).extend({ throttle: 500 }).subscribe(function () { viewModel.dataSource.changed.fire(); }); return viewModel; };
Переменные skip и PAGE_SIZE необходимы для загрузки части данных (в данном случае 10 записей), а дозагрузка будет идти по мере необходимости.
Переменные searchString и showSearch для поиска, при чем поиск срабатывает с пол секундной задержкой после ввода символа.
Результат:
Ну и напоследок, выведем информацию о выбранном покупателе:
View:
<div data-options="dxView : { name: 'customer-details', title: 'Product' } " > <div data-options="dxContent : { targetPlaceholder: 'content' } " > <div class="dx-fieldset"> <div class="dx-field"> <div class="dx-field-label">Id: </div> <div class="dx-field-value" data-bind="text: id"></div> </div> <div class="dx-field"> <div class="dx-field-label">Name: </div> <div class="dx-field-value" data-bind="text: name"></div> </div> <div class="dx-field"> <div class="dx-field-label">Address: </div> <div class="dx-field-value" data-bind="text: address"></div> </div> <div class="dx-field"> <div class="dx-field-label">Comment: </div> <div class="dx-field-value" data-bind="text: comment"></div> </div> </div> </div> </div>
ViewModel:
MyApp['customer-details'] = function (params) { var viewModel = { id: parseInt(params.id), name: ko.observable(''), address: ko.observable(''), comment:ko.observable('') }; var data = MyApp.manager.getEntityByKey("Customer", viewModel.id); console.log(data); viewModel.name(data.CustomerName()); viewModel.address(data.Address()); viewModel.comment(data.Comment()); return viewModel; };
Примечание: скриншоты сделаны с эмулятора Ripple Emulator (Beta).
Резюме.
Мы получили довольно просто полноценное SPA приложение для мобильных устройств с навигацией и загрузкой данных. На данный момент сложно судить о качестве/скорости/ и т.д. приложения, поэтому в следующей статье я немного расширю функционал и выложу на Azure, что бы каждый желающий смог попробовать.
ссылка на оригинал статьи http://habrahabr.ru/post/185316/
Добавить комментарий