📜 ⬆️ ⬇️

HTML5 mobile application: error or success. Attempt # 0

For several years, reading news and events in the world of Web development, I had a pink dream: I wrote once - it works everywhere and always. At the same time, I often meet with negative reviews about the development of mobile applications on HTML5 ( here are comments on articles 1 and 2 ). The main arguments of the strikers are: inconsistency with the native interface, glitchiness and inhibition, problems with data storage, etc., etc. In no case do not want to start the next Kholi Vary on this topic. But the dream lives and it can be confirmed or rejected only after its own attack on the rake.
So, the goal is to write on HTML5 a mobile application for collecting orders by a sales agent in retail outlets. I came across these solutions from different companies, so I am familiar with the subject area, and this topic is ideal for a dream.

To the basic requirements, I will add a few notes from my own experience:

Small note: The article was written with the aim of consolidating the material studied on the study of new technology. In connection with the complete lack of real experience in creating applications of this kind, I apologize in advance for any flaws.

Preliminary architecture:

Backend - .net MVC with OData. Globally it does not matter what I will use in this role, as long as it complies with the new WEB API standards. Frontend - everything is difficult for me. In the absence of experience, it is very difficult to choose something. After some browsing, I stopped at PhoneJS . I was bribed by the fact that this is a full-fledged framework for a SPA application, so it is not necessary to link as far as libraries to a heap, as well as the use of knockoutjs. I decided to use breeze to work with data. I am sure that the list will change during the development process. All this is then packaged with PhoneGap and get the similarity of the application.
In this article we will build something simple to begin with: viewing the data of an outlet on a particular route of a sales agent.

Creating a project.

Create a new ASP.NET MVC 4 Web Application project and name it “ MSales ”. In the New ASP.NET MVC 4 Project dialog , select the Web API template.
Update packages: Update-Package knockoutjs Update-Package jQuery , and install: Install-Package Breeze.WebApi Install-Package datajs .
Unfortunately, there is no package for PhoneJS, so with pens we add all the necessary css and js to the project. There are several layout types to choose from, I used NavbarLayout , changing the _Layout.cshtml file:
 <!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> 

In the BundleConfig file we write all the content and scripts. I did it like this:
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")); 


')
Model and controllers

At the moment, we will include two files in the model: classes for routes (a sales agent follows these routes) and outlets (stores):
Model
 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; } } 


The controllers will be very simple (you can read more about OData here ):
Controllers
 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); } } 



A small touch in the WebApiConfig file:
 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(); } } 


When registering a route for the OData protocol, you must specify the line builder.Namespace = "MSales.Models"; required for the breeze and datajs libraries to work.

Frontend.

In the Scripts / app folder, create an app.init.js script file to initialize the libraries:
 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); }); 

We create an HTML application in which we specify the layout and navigation parameters, which consists of two points: routes and about; and also initialize the breeze library.
In the Index.cshtml file, you must place a dxView and a special area called “content”, which displays the usual list:
 <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> 

In order for these few lines to work, you need to create a Viewmodel, so in the Scripts / app folder, create the file 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; }; 

I want to note that the name of the Viewmodel is the same as the name of dxView, and contains only the dataSource object, in which we define one load method for loading data. The refresh parameter determines whether the widget should be updated completely. In the method we build a query, sorting by the RouteID field and executing it.
Add another 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> 

Result for IPhone:
image
You probably noticed that the dxAction: '#customers/{RouteID}' event is hung on the list item, where, according to the specified navigation, '#customers is the View called, and RouteID is the parameter passed to this 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> 

Due to the fact that there may be a lot of buyers, the ability to search has been added: the dxCommand is added - the search button that calls the find function, and the input field in front of the list.
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; }; 

The variables skip and PAGE_SIZE are needed to load part of the data (in this case, 10 records), and reloading will go as needed.
The searchString and showSearch variables are for search, and the search is triggered with a half-second delay after entering the character.
Result:
image
And finally, we will display information about the selected customer:
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; }; 

image
Note: Screenshots are made from the Ripple Emulator (Beta) emulator.

Summary.

We got quite a simple full-fledged SPA application for mobile devices with navigation and data loading. At the moment it is difficult to judge the quality / speed / etc. applications, so in the next article I will expand the functionality a bit and put it on Azure, so that everyone could try.

Source: https://habr.com/ru/post/185316/


All Articles