Win 8.1 App использование HTML & WinJS tutorial

OSzone.net » Microsoft » Разработка приложений » Windows (до Windows 10) » Win 8.1 App использование HTML & WinJS tutorial
Иcточник: Habrahabr.ru
Опубликована: 08.04.2014
Я предполагаю, что эта статья будет интересна тем, кто знает и умеет HTML&JavaScript, но не пробовал силы в разработке приложений для Win8. Для того, чтобы пройти эту статью и кодить в сласть необходимо иметь на борту VS 2013.

В статье будут рассмотрены ключевые аспекты разработки приложений для платформы Win 8.1. А именно:

  1. Жизненный цикл работы приложения;
  2. Promise;
  3. Работа с DataSource;
  4. Создание собственных контролов;
  5. Работа с темплейтами;
  6. Tile-ы;
  7. Share;

Для тех кто не любит читать, как я, например, исходники я выложил на github.com/Sigura/HubraWin,

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

Если вы уже смотрите исходники, то обратите внимание, что я немного изменил default.js, для того чтоб там не было общего кода по запуску приложения и вынес его в app.js. Оставив в default.js только настройки и непосредственно запуск. Так же я дополнил WinJs.Utilities скромным набором «удобств» и шиной сообщений.

Работа с объектами


В пространстве WinJS есть специальный набор способов создать класс, добавить ему методов, расширить и сделать доступным.

например, объявление класса — шины сообщений:
// делаем замыкание, так же для того чтоб описать зависимости
// и иметь возможность подменить их, если когда-нибудь мы захотим использовать
// этот код в другом приложении
(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


Фактически это web приложение у которого есть свой хостинг (WWAHost.exe). Свой framework для работы с ресурсами OS и приложения в пространстве имён WinJS, Application, Windows, … И набор контролов в WinJS.UI.
Я сделал «свой» класс для приложения, для того чтоб использовать его в других проектах. По мимо стандартного набора настроек этот класс создаёт события для обработки запуска (activated с информацией о запуске), прекращения работы и прочего (oncheckpoint, before-start, after-start).
класс приложения
; (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);

Тогда сам запуск приложения (default.js) будет выглядеть так:

; (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);


Навигация по страницам


Я сторонник приложений на одной «странице». WinJS предлагает богатый набор возможностей для реализации современных сценариев взаимодействия с пользователем.
Web aka WinJS приложение нуждается в отдельном объекте для обслуживания истории переходов, по страницам, обслуживания жизненного цикла страницы.
Т.е. каждую страницу при переходе на неё нам необходимо будет рендерить в её элемент, обязательно избавляясь от предыдущей, а именно убирая слушателей событий, открытые ресурсы и т.д.
Как должен выглядеть жизненный цикл страницы:

Контрол для обслуживания навигации
(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);

Код страницы
(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.appBar.show();
});
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) {
/// <param name="element" domElement="true" />

// 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._eventHandlerRemover = [];
}
});
})(WinJS);

Локализация


Если сделать файл strings\ru-RU\resources.resjson

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

то в коде пользоваться ссылками:
<span class="pagetitle" data-win-res="{ textContent: 'pageHeader' }"></span>

Путь к наиболее подходящему языку будет автоматически подхвачен при запуске.
Любопытно, что можно встроить в ресурсы байдинг.
Так же интересно, что для ресурсов используется специальный тип контента в jsproj
<PRIResource Include="strings\ru-RU\resources.resjson" />

Т.е. необходимо создать ресурсный файл пользуясь интерфейсом VS, совсем нельзя переименовать существующий файл, напрмиер, txt в resjson, он будет в jsproj:
<Content Include=" strings\ru-RU\resources.resjson" />

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

Темплейты, байдинг


Пример темплейта:
<div class="client-search-item-template" data-win-control="WinJS.Binding.Template" style="display: none;">
    <div class="client-item">
        <div class="client-info">
            <div class="client-photo"><img data-win-bind="alt: name; src: this HabraWin.Converters.clientPhoto;" /></div>
            <div class="client-name" data-win-bind="innerText: name"></div>
        </div>
        <div class="decoration-bottom-line"></div>
    </div>
</div>

Это разметка для отображения пользователя в списке. Специальным атрибутом (data-win-bind) указываются привязка к тому или иному свойству элемента, а также выражение для доступа к данным.
А для того чтоб произвести некоторые преобразования, например, для того, чтоб показать фотографию клиента можно указать конвертер:
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);

Для того чтоб применить к темплейту данные достаточно:

WinJS.Binding.processAll(element, data);

Контролы


Создание контрола в WinJS очень похоже на создание класса. Например, форма HabraWin.ClientSearchForm:
<form role="form" id="main-search-form" data-win-control="HabraWin.ClientSearchForm">
    <div class="form-group"><label for="second-name"><span data-win-res="{textContent: 'serachFormSecondNameLabel'}"></span></label><input data-win-res="{attributes: { 'placeholder' : 'serachFormSecondNamePlaceholder' }}" spellcheck="true" type="text" name="secondName" id="second-name" /></div>
    <div class="main-search-form-buttons form-group">
        <button type="button" name="search" class="button" id="search"><span data-win-res="{textContent: 'serachFormSearchButton'}"></span></button>
        <button type="reset" name="clear" class="button" id="clear"><span data-win-res="{textContent: 'serachFormClearButton'}"></span></button>
    </div>
</form>

Код для обслуживания событий и элементов управления формы
; (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;

for (var lbl in this.fields)
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.changedFields = [];
},
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 на примере Share


Если вы пользовались api для асинхронных вызовов, например, XmlHttpRequest и вам надо было выполнить цепочку зависимых друг от друга вызовов, то вы обращали внимание на то что такую цепочку вызовов сложно поддерживать, т.е. читать и изменять в первую очередь из-за вложенности. Я знаю два паттерна, которые могут избавить от вложенности: события или promise.

Например, объединение последовательных вызовов:

        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();

                });
        },


Доступ к данным — DataSource


Для визуализации данных можно использовать WinJs.UI.ListView. Например, этот замечательный контрол умеет загружать данные не все сразу, а по мере необходимости отображать. Что бережет ресурсы при отображении более сотни записей. Но для этого необходимо реализовать свой DataSource с поддержкой загрузки данных постранично.

Пример кода DataSource для постраничной загрузки пользователей
; (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


В Win8 есть замечательная возможность для приложений, которые пользователь добавил себе на стартовую панель, показывать наиболее ценную информацию в тот или иной момент.
В примере ниже я использую темплейт TileWideSmallImageAndText03, все возможные варианты темплейтов можно увидеть на msdn
Пример кода для обновления tile-ов:

; (function(winJs, ui, dom) {

    winJs.Namespace.define('HabraWin', {
        Tile: {
           // создаём xml для tile-а
            wideSmallImageAndText03: function(img, text) {
                var tileXmlString = '<tile><visual version="1" lang="ru-RU" branding="logo">'
                    + '<binding template="TileWideSmallImageAndText03">'
                    + '<image id="1" src="' + img + '" alt="logo" />'
                    + '<text id="1">' + text + '</text>'
                    + '</binding>'
                    + '</visual></tile>';

                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);

Ссылка: http://www.oszone.net/23752/HTML-WinJS