Я предполагаю, что эта статья будет интересна тем, кто знает и умеет HTML&JavaScript, но не пробовал силы в разработке приложений для Win8. Для того, чтобы пройти эту статью и кодить в сласть необходимо иметь на борту VS 2013.
В статье будут рассмотрены ключевые аспекты разработки приложений для платформы Win 8.1. А именно:
Жизненный цикл работы приложения;
Promise;
Работа с DataSource;
Создание собственных контролов;
Работа с темплейтами;
Tile-ы;
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) {
Я сторонник приложений на одной «странице». WinJS предлагает богатый набор возможностей для реализации современных сценариев взаимодействия с пользователем. Web aka WinJS приложение нуждается в отдельном объекте для обслуживания истории переходов, по страницам, обслуживания жизненного цикла страницы. Т.е. каждую страницу при переходе на неё нам необходимо будет рендерить в её элемент, обязательно избавляясь от предыдущей, а именно убирая слушателей событий, открытые ресурсы и т.д. Как должен выглядеть жизненный цикл страницы:
Применение ресурсов, байдингов (processed),
Инициализация (ready), срабатывает тогда, когда готовы все контролы на странице и применены ресурсы, здесь можно:
Подписаться на события контролов страницы, в том числе AppBar,
Сделать какую-то работу, например первый поиск,
Обработка обновления страницы, например, при изменении размеров,
Выгрузка страницы (unload) для очистки всех обработчиков событий, и прочих ресурсов.
Контрол для обслуживания навигации
(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.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;
Путь к наиболее подходящему языку будет автоматически подхвачен при запуске. Любопытно, что можно встроить в ресурсы байдинг. Так же интересно, что для ресурсов используется специальный тип контента в jsproj
Т.е. необходимо создать ресурсный файл пользуясь интерфейсом VS, совсем нельзя переименовать существующий файл, напрмиер, txt в resjson, он будет в jsproj:
Это разметка для отображения пользователя в списке. Специальным атрибутом (data-win-bind) указываются привязка к тому или иному свойству элемента, а также выражение для доступа к данным. А для того чтоб произвести некоторые преобразования, например, для того, чтоб показать фотографию клиента можно указать конвертер: src: this HabraWin.Converters.clientPhoto
Код для обслуживания событий и элементов управления формы
; (function (winJs) { 'use strict';
var searchForm = winJs.Class.derive(HabraWin.BaseForm, winJs.Utilities.defaultControlConstructor(), { init: function (element, options) { var me = this;
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;
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 () { } });
Если вы пользовались 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 для постраничной загрузки пользователей
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); };
В 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);