Win 8.1 App using HTML & WinJS

  • Tutorial
I assume that this article will be interesting to those who know and can HTML & JavaScript, but have not tried their hand at developing applications for Win8. In order to go through this article and encode it, you must have VS 2013 on board.

The article will discuss key aspects of developing applications for the Win 8.1 platform. Namely:

  1. Application life cycle
  2. Promise
  3. Work with DataSource;
  4. Creating your own controls;
  5. Work with templates;
  6. Tiles
  7. Share;

For those who do not like to read, like I, for example, I posted the source code on github.com/Sigura/HubraWin .

In order to reveal all the topics I identified, I created an application that will display a list of contacts.

If you are already looking at the sources, then note that I changed default.js a bit so that there is no general code for launching the application and put it out in app.js. Leaving only default settings and start directly in default.js. I also added WinJs.Utilities with a modest set of "amenities" and a message bus.

Work with objects


In the WinJS space, there is a special set of ways to create a class, add methods to it, extend and make it available.

for example, a class declaration - message bus:
// делаем замыкание, так же для того чтоб описать зависимости
// и иметь возможность подменить их, если когда-нибудь мы захотим использовать
// этот код в другом приложении
(function (winJs) {
    'use strict';
// создаём класс
    var bus = winJs.Class.define(winJs.Utilities.defaultConstructor(), {
        init: function (element, options) {
            var me = this;
        }
    });
// добавляем в него возможность отправлять и принимать сообщения
    winJs.Class.mix(bus, winJs.Utilities.eventMixin);
// добавляем шину в общий доступ 
    winJs.Namespace.define('HabraWin', {
        MessageBus: bus
    });
})(WinJS);


WinJS application


In fact, this is a web application that has its own hosting (WWAHost.exe). Own framework for working with OS resources and applications in the WinJS, Application, Windows, ... namespace and a set of controls in WinJS.UI.
I made my own class for the application in order to use it in other projects. In addition to the standard set of settings, this class creates events for launch processing (activated with information about the launch), termination of work and other things (oncheckpoint, before-start, after-start).
application class
; (function (winJs, habraWin) {
 
    var app = winJs.Class.define (winJs.Utilities.defaultConstructor (), {
        init: function (options) {
            var me = this;
            var activatedEventType = 'activated';
            var ui = options. ui;
            var application = options.app;
            var nav = options.nav;
            var activation = options.activation;
            var sched = options.sched;
 
            application.addEventListener (activatedEventType, function (args) {
 
                me.dispatchEvent (activatedEventType, {
                    kind: args.detail.kind,
                    isReactivated: args.detail.previousExecutionState === activation.ApplicationExecutionState.terminated,
                    parevEventDetails: args.detail
                });
 
                if (args.detail.kind! == activation.ActivationKind.launch)
                    return;
 
                nav.history = application.sessionState.history || {};
                nav.history.current.initialPlaceholder = true;
 
                ui.disableAnimations ();
                var p = ui.processAll (). then (function () {
                    return nav.navigate (nav.location || habraWin.navigator.home, nav.state);
                }). then (function () {
                    return sched.requestDrain (sched.Priority.aboveNormal + 1);
                }). then (function () {
                    ui.enableAnimations ();
                });
 
                args.setPromise (p);
            });
 
            application.oncheckpoint = function (args) {
                me.dispatchEvent ('oncheckpoint', {prevEvent: args});
                application.sessionState.history = nav.history;
            };
        },
        start: function () {
            var me = this;
 
            me.dispatchEvent ('before-start', me);
 
            me.options.app.start ();
 
            me.dispatchEvent ('after-start', me);
 
        }
    });
 
    winJs.Class.mix (app, winJs.Utilities.eventMixin);
 
    winJs.Namespace.define ('Application', {
        Instance: app
    });
 
}) (WinJS, HabraWin);

Then the launch of the application (default.js) will look like this:

; (function (application, winJs, habraWin, windows, window) {
    'use strict';
    winJs.Binding.optimizeBindingReferences = true;
    // создаём наше приложение
    var app = new application.Instance({
        activation: windows.ApplicationModel.Activation,
        app: winJs.Application,
        nav: winJs.Navigation,
        sched: winJs.Utilities.Scheduler,
        ui: winJs.UI
    });
    // делаем доступной шину сообщений в пространстве WinJS
    winJs.bus = new habraWin.MessageBus();
    // делаем приложение 
    window.app = app;
    // запускаем
    app.start();
})(Application, WinJS, HabraWin, Windows, window);


Page navigation


I am a supporter of applications on one "page". WinJS offers a rich set of features for implementing modern user interaction scenarios.
Web aka WinJS application needs a separate object to serve the history of navigation, page by page, maintenance of the page life cycle.
Those. each page upon transition to it we will need to render into its element, be sure to get rid of the previous one, namely removing event listeners, open resources, etc.
What the page life cycle should look like:
  • The use of resources, biding (processed),
  • Initialization (ready) is triggered when all controls on the page are ready and resources are applied, here you can:
    1. Subscribe to page controls events, including AppBar,
    2. Do some work, such as a first search,
  • Processing page refresh, for example, when resizing,
  • Page unloading to clear all event handlers and other resources.

Control for navigation service
(function (winJs, habraWin) {
    'use strict';
 
    winJs.Namespace.define ('HabraWin', {
        PageNavigatorControl: winJs.Class.define (
            function (element, options) {
                var nav = winJs.Navigation;
 
                this._element = element || document.createElement ('div');
                this._element.appendChild (this._createPageElement ());
 
                this.home = options.home;
 
                this._eventHandlerRemover = [];
 
                this.addRemovableEventListener (nav, 'navigating', this._navigating.bind (this), false);
                this.addRemovableEventListener (nav, 'navigated', this._navigated.bind (this), false);
 
                window.onresize = this._resized.bind (this);
 
                habraWin.navigator = this;
            }, {
                addRemovableEventListener: function (e, eventName, handler, capture) {
                    var that = this;
 
                    e.addEventListener (eventName, handler, capture);
 
                    that._eventHandlerRemover.push (function () {
                        e.removeEventListener (eventName, handler);
                    });
                },
                home: '',
                _element: null,
                _lastNavigationPromise: winJs.Promise.as (),
                _lastViewstate: 0,
 
                pageControl: {
                    get: function () {return this.pageElement && this.pageElement.winControl; }
                },
 
                pageElement: {
                    get: function () {return this._element.firstElementChild; }
                },
 
                dispose: function () {
                    if (this._disposed) {
                        return;
                    }
 
                    this._disposed = true;
                    winJs.Utilities.disposeSubTree (this._element);
                    for (var i = 0; i <this._eventHandlerRemover.length; i ++) {
                        this._eventHandlerRemover [i] ();
                    }
                    this._eventHandlerRemover = null;
                },
 
                _createPageElement: function () {
                    var element = document.createElement ('div');
                    element.setAttribute ('dir', window.getComputedStyle (this._element, null) .direction);
                    element.style.position = 'absolute';
                    element.style.visibility = 'hidden';
                    element.style.width = '100%';
                    element.style.height = '100%';
                    return element;
                },
 
                _getAnimationElements: function () {
                    if (this.pageControl && this.pageControl.getAnimationElements) {
                        return this.pageControl.getAnimationElements ();
                    }
                    return this.pageElement;
                },
 
                _navigated: function () {
                    this.pageElement.style.visibility = '';
                    winJs.UI.Animation.enterPage (this._getAnimationElements ()). done ();
                },
 
                _navigating: function (args) {
                    var newElement = this._createPageElement ();
                    this._element.appendChild (newElement);
 
                    this._lastNavigationPromise.cancel ();
 
                    var me = this;
                    this._lastNavigationPromise = winJs.Promise.as (). then (function () {
                        return winJs.UI.Pages.render (args.detail.location, newElement, args.detail.state);
                    }). then (function parentElement ( control) {
                        var oldElement = me.pageElement;
                        if (oldElement.winControl) {
                            if (oldElement.winControl.unload) {
                                oldElement.winControl.unload ();
                            }
                            oldElement.winControl.dispose ();
                        }
                        oldElement.parentNode.removeChild (oldElement);
                        oldElement.innerText = '';
                    });
 
                    args.detail.setPromise (this._lastNavigationPromise);
                },
 
                _resized: function (args) {
                    if (this.pageControl && this.pageControl.updateLayout) {
                        this.pageControl.updateLayout.call (this.pageControl, this.pageElement);
                    }
                }
            }
        )
    });
}) (WinJS, HabraWin);

Page Code
(function (winJs) {
    'use strict';
 
    winJs.UI.Pages.define ('/ pages / hub / hub.html', {
        processed: function (element) {
            return winJs.Resources.processAll (element);
        },
        className: 'client-search-hub',
        ready: function (element, options) {
            this.initEnv ();
 
            this.initAppBar (element);
            this.subscribe (element);
 
            this.setFormValues ​​(options);
 
            this.search ( );
        },
        initAppBar: function (element) {
            var me = this;
            me.appBar = element.querySelector ('# appbar'). winControl;
 
            this.addRemovableEventListener (me.appBar.getCommandById ('clear'), 'click', function () {
                winJs.bus.dispatchEvent ('clear-command');
            }, false);
        },
        subscribe: function () {
            var me = this;
 
            var search = me.element.querySelector ('# search');
 
            search && me.addRemovableEventListener (search, 'click', me.search.bind (me), false);
 
            me.addRemovableEventListener (winJs.bus, 'client-selected', function (item) {
                me.currentClient = item.detail.data;
                //me.editButton.disabled = false;
                me.appBar.sticky = true;

            });
            me.addRemovableEventListener (winJs.bus, 'client-unselected', function (item) {
                me.currentClient = null;
                //me.editButton.disabled = true;
                me.appBar.hide ();
                me.appBar.sticky = false ;
            });
        },
        unload: function () {
            this.element.classList.remove (this.className);
 
            if (this._disposed) {
                return;
            }
 
            this._disposed = true;
            winJs.Utilities.disposeSubTree (this._element);
            for (var i = 0; i <this._eventHandlerRemover.length; ++ i) {
                this._eventHandlerRemover [i] ();
            }
            this._eventHandlerRemover = null;
        },
        addRemovableEventListener: function (e, eventName, handler, capture) {
            capture = capture! == false? false: true;
 
            e.addEventListener (eventName, handler, capture);
 
            this._eventHandlerRemover.push (function () {
                e.removeEventListener (eventName, handler);
            });
        },
        updateLayout: function (element) {
            /// 
 
            // TODO: Respond to changes in layout.
 
            // debugger;
        },
        setFormValues: function (clinetInfo) {
            this.searchForm = this.element.querySelector ('# main-search-form');
 
            this.searchForm && this.searchForm.setAttribute ('data-win-options', JSON.stringify (clinetInfo));
            this.searchForm && this.searchForm.winControl && this.searchForm.winControl.setValues ​​(clinetInfo);
        },
        search: function () {
            winJs.bus.dispatchEvent ('search-command');
        },
        initEnv: function () {
            this.element.classList.add (this.className);
            this.
        }
    });
}) (WinJS);

Localization


If you make the file strings \ ru-RU \ resources.resjson

{
	"pageHeader": "Habra WinJS 8.1"
// …
}

then in the code use the links:

The path to the most suitable language will be automatically picked up at startup.
It is curious what can be embedded in resources.
It’s also interesting that resources use a special type of content in jsproj

Those. you need to create a resource file using the VS interface, you can’t completely rename an existing file, for example, txt in resjson, it will be in jsproj:

which will make it impossible to use because it will not load automatically.

Templates, Biding


Template example:

This is the markup to display the user in the list. A special attribute (data-win-bind) indicates the binding to a particular property of the element, as well as an expression for accessing data.
And in order to make some transformations, for example, in order to show the client’s photo you can specify the converter:
src: this HabraWin.Converters.clientPhoto

; (function (winJs) {
    'use strict';
    var converters = {
        clientPhoto: winJs.Binding.converter(function (client) {
            if (!client || !client.hasPhoto)
                return '/images/empty-photo.png';
            return converters.baseAddress + '/clients/photos/' + client.ID;
        })
    };
    winJs.Namespace.define('HabraWin', { Converters: converters });
})(WinJS);

In order to apply data to the template, it is enough:

WinJS.Binding.processAll(element, data);

Controls


Creating control in WinJS is very similar to creating a class. For example, the form HabraWin.ClientSearchForm:

Code for servicing events and form controls
; (function (winJs) {
    'use strict';
 
    var searchForm = winJs.Class.derive (HabraWin.BaseForm, winJs.Utilities.defaultControlConstructor (), {
        init: function (element, options) {
            var me = this;
 
            me.initProperies ();
 
            me.clearForm ();
 
            me.defineElements (element);
            me.defineEvents ();
            me.subscribe ();
 
            me.setValues ​​(options);
            me.search ();
        },
        defineElements: function (element) {
            var me = this;
 
            me.fields = {
                secondName: element.querySelector ('input [name = secondName]')
            };
 
            me.buttons = {
                clear: element.querySelector ('button [name = clear]') //,
            };
 
 
            var values ​​= this.getValues ​​();
 
            this.oldValues ​​= JSON.stringify (values);
        },
        defineEvents: function () {
            var me = this;
 
            me.buttons.clear.addEventListener ('click', me.clearAndSearch.bind (me));
        },
        setValues: function (values) {
            if (! values) {
                return;
            }
            this.changedFields = [];
 
            for (var lbl in values)
                if (values.hasOwnProperty (lbl) && this.fields.hasOwnProperty (lbl)) {
                    var field = this.fields [lbl];
                    var value = values ​​[lbl];
                    var valPropName = field && ('type' in field) && field.type === 'checkbox'? 'checked': (field && 'value' in field? 'value': 'current');
 
                    if (! field) {
                        continue;
                    }
                    field [valPropName] = value;
                    value && this.changedFields.push (lbl);
                }
        },
        subscribe: function () {
            var me = this;
 

                if (this.fields.hasOwnProperty (lbl)) {
                    var field = this.fields [lbl];
                    var isTextField = 'value' in field;
 
                    field.addEventListener (isTextField? 'keydown': 'change', me.fieldChanged.bind (me));
                    field.addEventListener (isTextField? 'keydown': 'change', isTextField? me.search.bind (me) .defer (1000): me.search.bind (me));
 
                    if (isTextField) {
                        ['cut', 'paste', 'change']. forEach (function (e) {
                            field.addEventListener (e, me.fieldChanged.bind (me));
                        });


 
            winJs.bus.addEventListener ('clear-command', me.clearAndSearch.bind (me));
        },
        clearAndSearch: function () {
            this.clearForm ();
            this.search ();
        },
        addNewClient: function () {
            var values ​​= this.getValues ​​();
 
            winJs.Navigation.navigate ("/ pages / section / section.html", values);
        },
        getValues: function () {
            var me = this;
            var values ​​= {};
 
            this.changedFields.forEach (function (lbl) {
                values ​​[lbl] = me.getValue (lbl);
            });
 
            return values;
        },
        search: function () {
            var values ​​= this.getValues ​​();
 
            winJs.bus.dispatchEvent ('search-client', values);
 
            this.oldValues ​​= JSON.stringify (values);
        },
        clearForm: function () {
            var me = this;
 
            var fields = Array.prototype.slice.call (me.element.querySelectorAll ('input [type = text], select'), 0);
 
            fields.forEach (function (e) {
                e.value = '';
            });
 
            var current = new Date ();
 
            me.fields && me.fields.birthday && (me.fields.birthday.current = new Date (current.setYear (current.getFullYear () - 24)));
 
            this.
        },
        fieldLabel: function (field) {
            return field && (field.getAttribute ('name') || field.id);
        },
        fieldChanged: function (e) {
            var field = e && e.currentTarget;
            var lbl = this.fieldLabel (field);
 
            if (! (lbl in this.fields))
                return;
 
            var value = this.getValue (lbl);
 
            if (! value) {
                this.changedFields.remove (lbl);
                return
            }
 
            if (this.changedFields.indexOf (lbl) === -1) {
                this.changedFields.push (lbl);
            }
        },
        initProperies: function () {
        }
    });
 
    winJs.Namespace.define ('HabraWin', {
        ClientSearchForm: searchForm
    });
 
}) (WinJS);


Promise on the example of Share


If you used api for asynchronous calls, for example, XmlHttpRequest and you had to execute a chain of calls dependent on each other, then you paid attention to the fact that such a chain of calls is difficult to maintain, i.e. read and modify primarily due to nesting. I know two patterns that can save you from nesting: events or promise.

For example, combining consecutive calls:

        share: function(e) {
            var request = e.request;
            var deferral = request.getDeferral();
            var offering = this.offering;
            var files = [];
            var me = this;
            var text = offering.description.replace(/<[^>]+>/gim, '').replace(/ [\s]+/, ' ');
            // запускаем асинхронную операцию:
            this.fileListControl.selection.getItems()
                .then(function (items) {
                    // собираем доступные файлы, тоже асинхронно
                    return items.map(function (item) {
                        var uri = new Windows.Foundation.Uri(item.data.uri);
                        return Windows.Storage.StorageFile.getFileFromApplicationUriAsync(uri)
                            .then(function (storageFile) {
                                files.push(storageFile);
                            });
                    });
                }).then(function (promises) {
                    // соединяем все операции, чтоб работать с их результатами
                    return WinJS.Promise.join(promises);
                }).done(function () {
                    // формируем пакет данных для того чтоб поделиться ими с другими приложениями
                    request.data.properties.title = offering.name;
                    request.data.properties.description = text;
                    if (files.length)
                        request.data.setStorageItems(files);
                    else
                        me.articlePackage(request.data);
                    deferral.complete();
                });
        },


Data Access - DataSource


To visualize data, you can use WinJs.UI.ListView. For example, this wonderful control can load data not all at once, but display it as necessary. That saves resources when displaying more than a hundred entries. But for this you need to implement your DataSource with support for loading data page by page.

Example DataSource Code for Paging Users
; (function (winJs, console) {
    'use strict';
 
    var clientSearchDataAdapter = winJs.Class.define(winJs.Utilities.defaultConstructor(), {
        def: {
            maxCount: 300,
            maxPageSize: 50,
            minPageSize: 50
        },
        init: function (options) {
            this.cache = {};
            this._filter = null;
 
            this.dataSource = options.dataSource;
        },
        condition: {
            get: function () {
                return this._filter;
            },
            set: function (value) {
 
                this._filter = value;
 
                this.dataSource && this.dataSource.invalidateAll && this.dataSource.invalidateAll();
 
                return value;
            }
        },
        getQuery: function () {
            var me = this;
 
            return new HabraWin.ProxyBuilder('client').then(function (proxy) {
                return proxy.search(me.condition);
            });
        },
        getCount: function () {
 
            var me = this;
            var cacheKey = JSON.stringify(me.condition);
 
            if (cacheKey in this.cache)
                return WinJS.Promise.wrap(me.cache[cacheKey].length);
 
            var query = me.getQuery();
            var i = 0;
 
            return query
                .then(function (clients) {
                    me.cache[cacheKey] = clients.map(function (item) {
                        return {
                            key: '' + (i++),
                            data: item,
                            groupKey: item.secondName.length > 0 ? item.secondName.substring(0, 1).toUpperCase() : '-'
                        };
                    });
 
                    var filtered = me.applyFilters({ items: clients, offset: 0, totalCount: clients.length });
 
                    return filtered.items.length;
                });
        },
        addFilter: function (filter) {
            this.filters = this.filters || [];
 
            this.filters.push(filter);
        },
        applyFilters: function (result) {
 
            if (!this.filters || !this.filters.length)
                return result;
 
            var me = this;
 
            this.filters.forEach(function (filter) {
                result = filter(result, me.condition);
            });
 
            return result;
        },
        itemsFromIndex: function (requestIndex, countBefore, countAfter) {
            var me = this;
 
            if (requestIndex >= me.options.maxCount) {
                return winJs.Promise.wrapError(new winJs.ErrorFromName(winJs.UI.FetchError.doesNotExist));
            }
 
            var fetchSize, fetchIndex;
            if (countBefore > countAfter) {
                countAfter = Math.min(countAfter, 10);
                var fetchBefore = Math.max(Math.min(countBefore, me.options.maxPageSize - (countAfter + 1)), me.options.minPageSize - (countAfter + 1));
                fetchSize = fetchBefore + countAfter + 1;
                fetchIndex = requestIndex - fetchBefore;
            } else {
                countBefore = Math.min(countBefore, 10);
                var fetchAfter = Math.max(Math.min(countAfter, me.options.maxPageSize - (countBefore + 1)), me.options.minPageSize - (countBefore + 1));
                fetchSize = countBefore + fetchAfter + 1;
                fetchIndex = requestIndex - countBefore;
            }
            var cacheKey = JSON.stringify(me.condition);
            var result = function () {
                var cache = me.cache[cacheKey];
                var items = cache.slice(fetchIndex, fetchIndex + fetchSize);
                var offset = requestIndex - fetchIndex;
                var totalCount = Math.min(cache.length, me.options.maxCount);
                var r = {
                    items: items,
                    offset: offset,
                    totalCount: totalCount,
                };
                var filtered = me.applyFilters(r);
 
                return filtered;
            };
 
            if (cacheKey in me.cache) {
                return WinJS.Promise.wrap(result());
            }
 
            var query = me.getQuery();
 
            return query
                .then(function (items) {
 
                    var i = 0;
 
                    me.cache[cacheKey] = items.map(function (item) {
                        return {
                            key: '' + (fetchIndex + i++),
                            data: item,
                            groupKey: item.secondName.length > 0 ? item.secondName.substring(0, 1).toUpperCase() : '-'
                        };
                    });
 
                    return result();
                });
        }
    });
 
    var clientsDataSource = winJs.Class.derive(winJs.UI.VirtualizedDataSource, function (condition) {
        var dataAdapter = new clientSearchDataAdapter({
            dataSource: this
        });
 
        this.setCondition = function (cond) {
            dataAdapter.condition = cond;
        };
 
        this.addFilter = function (filter) {
            dataAdapter.addFilter (filter);
        };
 
        this._baseDataSourceConstructor (dataAdapter);
 
        this.setCondition (condition);
    });
 
 
    winJs.Namespace.define ('HabraWin.DataSources', {
        ClientSearch: clientsDataSource
    });
 
}) (WinJS, console);


Tile


In Win8 there is a great opportunity for applications that the user has added to his start panel to display the most valuable information at one time or another.
In the example below, I use the TileWideSmallImageAndText03 template, all possible template options can be seen on msdn
Example code for updating tiles :

; (function(winJs, ui, dom) {
    winJs.Namespace.define('HabraWin', {
        Tile: {
           // создаём xml для tile-а
            wideSmallImageAndText03: function(img, text) {
                var tileXmlString = ''
                    + ''
                    + 'logo'
                    + '' + text + ''
                    + ''
                    + '';
                var tileDom = new dom.XmlDocument();
                tileDom.loadXml(tileXmlString);
                // делаем из xml сообщение
                return new ui.Notifications.TileNotification(tileDom);
            },
            baseUrl: '',
            // обновление tile-ов для приложения
            updateTile: function() {
                var tileUpdateManager = ui.Notifications.TileUpdateManager.createTileUpdaterForApplication();
                var me = this;
                var mesageAccepted = WinJS.Resources.getString('tileMessageAccepted').value;
                var mesageDenied = WinJS.Resources.getString('tileMessageDenied').value;
                tileUpdateManager.clear();
                tileUpdateManager.enableNotificationQueue(true);
                [
                    { Creator: { ID: '30BD3259-EF01-4ebb-ACEE-5065EB2885E1', Photo: true }, Description: mesageAccepted },
                    { Creator: { ID: 'A2021DFE-1271-41d1-9A90-A64039A8A5E6', Photo: true }, Description: mesageDenied }
                ].forEach(function(comment) {
                    var img = (comment.Creator && comment.Creator.Photo && (me.baseUrl + '/clients/photos/' + comment.Creator.ID)) || 'appx:///images/empty.png';
                    var text = (comment.Description) || '...';
                    var tile = me.wideSmallImageAndText03(img, text);
                    tileUpdateManager.update(tile);
                });
            }
        }
    });
})(WinJS, Windows.UI, Windows.Data.Xml.Dom);

Also popular now: