HTML5 INSIGHT

Jan 15

Введение в разработку WinRT-приложений на HTML/JavaScript. Улучшение работы с данными

Эта статья продолжает серию материалов (первая часть, вторая часть), посвященных азам разработки WinRT-приложений на HTML/JS для Windows 8. В этой части мы постараемся улучшить надежность получения и качество отображения данных, а также немного поговорим о контрактах.

image

Напомню, что в предыдущей части мы остановились на том, что научились получать данные из внешних RSS-потоков и изменили стили отображения данных для различных состояний приложения, включая snapped-режим.

Ограничение вывода данных

Если вы попробуете еще раз проследить, как получаются и отображаются данные, вы легко заметите, что мы их никак не ограничиваем. Приложение получает на входе потоки данных из RSS, преобразовывает их во внутреннюю структуру, используемую для связывания данных, и далее проецирует их в коллекцию на экране. Фактически, мы сразу видим все-все данные прямо с первого экрана.

Отмечу, что особенностью данного решения является то, что фактически мы всегда имеем на руках всю доступную информацию. В сложном проекте с бо́льшим количеством данных, возможно, серверной логикой и наличием специальных API, скорее всего, имело бы смысл запрашивать данные по мере необходимости, подгружая сначала только заголовки. Но это уже другая история. :)

В конечном счете, в нашем случае вместо того, чтобы показывать быстро и целиком всю картину последних событий, для чего было бы достаточным выводить 6-8 последних записей, мы сразу обрушиваем на пользователя весь поток новостей. Большой набор данных также не дает возможности быстро переключаться на соседние группы — приходится слишком долго прокручивать (забегая сильно вперед, скажу, что частично эту проблему сглаживает Semantic Zoom).

Таким образом, наша первоочередная задача — ограничить поток данных на первом экране.

Для этого откройте файл pages\groupedItems\groupedItems.js.

В начале функции в области переменных создайте еще две:

var groupedItems;
var tilesLimit = 6;

В первой мы будем хранить отфильтрованную коллекцию. Вторая указывает, сколько максимум плиток можно выводить (в идеале, подобные параметры нужно выносить в отдельные файлы настроек или аналогичные структуры данных, описывающих параметры проекта).

Далее найдите следующую строчку:

_initializeLayout: function (listView, viewState) {

Эта функция вызывается при инициализации страницы и прописывает, какие данные необходимо вывести на экран в зависимости от используемого режима отображения. В качестве источника данных используется глобальный объект Data, описанный в файле data.js.

Добавьте в начале функции следующие строчки:

            groupedItems = Data.items.createFiltered(function (item) {
                return item.index < tilesLimit;

            }).createGrouped(
                function groupKeySelector(item) { return item.group.key; },
                function groupDataSelector(item) { return item.group; }
            );

Напомню, что в свойстве items объекта Data прописана коллекция данных (WinJS.Binding.List), сгруппированных по принадлежности к тем или иным RSS-потокам.

Чтобы отфильтровать данные, мы берем эту коллекцию и оставляем только элементы, индекс которых меньше нашего ограничения, после чего заново группируем коллекцию в соответствии с группами.

Замечание: в данном случае мы явно ограничиваем количество элементов шестью (tilesLimit), однако, в общем случае это неправильный подход. На большом мониторе количество плиток может оказаться меньше желаемого, к тому же могут появляться одиноко висящие плитки в конце списка. Самое правильное: динамически рассчитывать лимит в зависимости от доступного пространства.

Далее в этой же функции замените встречающиеся ниже ссылки на Data и Data.items на переменную groupedItems:

            if (viewState === appViewState.snapped) {
                listView.itemDataSource = groupedItems.groups.dataSource;
                listView.groupDataSource = null;
                listView.layout = new ui.ListLayout();
            } else {
                listView.itemDataSource = groupedItems.dataSource;
                listView.groupDataSource = groupedItems.groups.dataSource;
                listView.layout = new ui.GridLayout({ groupHeaderPosition: "top" });
            }
Замечание: будьте внимательны с другими вхождениями Data на данной странице. В данном случае мы их не изменяем, так как они используются при переходе ко внутренним страницам, а выше мы всего лишь сделали фильтр по данным. Но в общем случае, если вы меняете источник данных, его, возможно, надо менять везде, либо, что правильнее, вынести отдельно слой работы с данными.

Запустите проект:

image

Отлично! Теперь мы сразу видим все самое свежее.

Проверка интернет-соединения

Следующий важный пункт: убедиться, что приложение корректно себя ведет при отсутствии интернет-соединения. Если вы попробуете запустить текущую версию приложения с отключенной сетью, например, включив Airplane-режим, оно должно вызвать ошибку из-за невозможности скачать нужные поток. (Возможно, они успели закешироваться, поэтому для надежности эксперимента можно подключить какие-нибудь другие потоки.)

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

Чтобы проверить наличие интернета, откройте файл js\data.js и добавьте в него следующую функцию:

    function isInternetConnection() {
        var connectionProfile = Windows.Networking.Connectivity.NetworkInformation.getInternetConnectionProfile();
        return (connectionProfile != null);
    }

Здесь мы обращаемся к WinRT, чтобы узнать текущее состояние сети. Это уже хорошая информация, хотя она и не дает 100% гарантии, что доступ к интернету и нужному потоку действительно есть.

Теперь давайте добавим функцию, которая будет выводить сообщение об ошибке при наличии проблем:

    function showConnectionError(msg, donotexit) {
        msg = (msg != undefined) ? msg : "";

        var messageDialog = new Windows.UI.Popups.MessageDialog(msg, "Can not connect");
        
        messageDialog.commands.append(new Windows.UI.Popups.UICommand("Ok", null, 1));

        messageDialog.showAsync().done(function (command) {

            if (!donotexit && command.id == 1) {
                MSApp.terminateApp({
                    number: 0,
                    stack: "",
                    description: "No internet connection"
                });
            }
        });
    }

Данная функция, используя WinRT API, выводит заданное сообщение об ошибке и, по умолчанию, если не выставлен флаг donotexit (или он равен false), завершает приложение.

Замечание: в реальном приложении логика поведения может отличаться. Например, приложение может намеренно кешировать данные или предлагать пользователю попробовать еще раз скачать потоки с сервера, если была временная потеря соединения.

Следующий шаг: запустить проверку наличия соединения, для этого замените строчку

    list = getBlogPosts(list);

на следующие:

    function tryUpdateData() {
        if (isInternetConnection()) {
            list = getBlogPosts(list);            
        } else {
            showConnectionError("Please check your internet connection. ");
        }
    };

    tryUpdateData();

Фактически, мы обернули обращение данных в проверку наличия интернет-соединения. Если его нет, приложение выдает сообщение об ошибке и закрывается:

image

Теперь давайте вернемся к функции getBlogPosts. Здесь тоже есть вероятность получения ошибки, например, если какой-то RSS-поток перестал работать, в этом случае наша обертка над XHR вызовет исключение, которое нужно перехватить.

Попробуйте заменить одну из ссылок на RSS на неправильную и запустить приложение (здесь мы предполагаем, что ответ сервера будет соответствующим неправильной ссылке).

Чтобы обработать исключительную ситуацию, в описании Promise нужно добавить функцию, вызываемую при возникновении ошибки. Сразу после внутренней анонимной функции function (response) {…} добавьте через запятую:

                function (error) {
                    showConnectionError("Can't get rss updates for " + feed.title + ". Used source: " + feed.url, true);
                }

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

Попробуйте сделать одну из ссылок неправильной и запустить приложение:

image

Ура! Теперь мы можем радовать пользователя злостными сообщениями. Тут я хочу повторить еще раз важную мысль: подобное информирование пользователя — это всего лишь полумера. В идеале нужно кешировать данные и информировать пользователя о наличии проблем с соединением менее разрушительным способом. См. например, как работает приложение Bing News в Windows 8.

Делимся информацией

Напоследок в этой статье мы научимся делиться информацией из приложения наружу, используя контракты общего доступа (sharing) и печати.

Откройте файл pages\itemDetail\itemDetail.js. Добавьте в начале переменную для хранения обработчика событий:

    var sharingHandler;

Добавьте также в начале или ближе к концу (вне WinJS.UI.Pages.define()) следующие функции:

1. setupSharing — регистрация на передачу данных

    function setupSharing(item) {
        var dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView();

        sharingHandler = onSharingDataRequested.bind(item);

        dataTransferManager.addEventListener("datarequested", sharingHandler);
    }

Здесь мы обращаемся через WinRT к менеджеру передачи данных и вешаем обработчик события запроса данных.

2. onSharingDataRequested — событие для обработки запроса

    function onSharingDataRequested(e) {
        var request = e.request;
        var item = this;
        var text = item.title;
        if (item.link) {
            text += "\n\r" + item.link;
            request.data.setUri(new Windows.Foundation.Uri(item.link));
        }
        request.data.properties.title = text;
        request.data.properties.description = "Some awesome news on Windows!";
        request.data.setText(text);
    }

Здесь мы описываем, какие данные мы передаем приложению-получателю: ссылку, текст и т.п. (подробности передачи данных можно посмотреть здесь: общий доступ к содержимому и его получение).

Внутри функции ready в самом конце добавьте вызов регистрации на поддержку контракта:

            setupSharing(item);

Запустите приложение, перейдите к посту и через панель чудо-кнопок попробуйте передать новость в другое приложение:

image

Осталось добавить еще одну очень важную деталь. Если вы попробуете вернуться на уровень выше (уйти с текущей страницы) и используете контракт общего доступа (Sharing), вы увидите, что он по-прежнему “реагирует” и предлагает пользователю несколько вариантов на выбор. Это не то, что мы бы ожидали в качестве правильного поведения приложения.

Чтобы такого не происходило, необходимо сбросить обработчики событий в менеджере передачи данных. Это можно делать на входе в каждую страницу, либо наоборот на выходе. Добавьте следующую функцию:

    function clearContracts() {
        var dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView();
        dataTransferManager.removeEventListener("datarequested", sharingHandler);
    }

Здесь мы просто удаляем соответствующий обработчик событий. Для вызова функции после описания свойства ready через запятую добавьте функцию для unload:

        unload: function () {
            clearContracts();
        }

Готово, теперь передача данных работает только там, где она и должна работать.

Печатаем статьи

Теперь давайте добавим поддержку печати. Прежде всего, надо отметить, что по умолчанию ваше приложение не умеет печатать. Для передачи документов на печать используется системный контракт печати (print), доступный пользователям через чудо-кнопку “Устройства”:

image

Чтобы добавить возможность печати в том же файле pages\itemDetail\itemDetail.js (это наш контекст печати), необходимо подписаться на соответствующий менеджер аналогично тому, как мы это делали для общего доступа. Добавьте переменную для хранения обработчика:

    var printTaskHandler;

функцию для подписи на контракт печати:

    function setupPrinting(item) {
        var printManager = Windows.Graphics.Printing.PrintManager.getForCurrentView();

        printTaskHandler = onPrintTaskRequested.bind(item)
        printManager.addEventListener("printtaskrequested", printTaskHandler);
    }

и обработчики событий:

    function onPrintTaskRequested(printEvent) {
        var printTask = printEvent.request.createPrintTask(this.group.title, function (args) {

            args.setSource(MSApp.getHtmlPrintDocumentSource(document));
        });
    }

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

Внутри функции ready в самом конце добавьте вызов регистрации на поддержку контракта:

            setupPrinting(item);

и не забудьте в clearContracts удалить обработчик запроса печати:

        var printManager = Windows.Graphics.Printing.PrintManager.getForCurrentView();
        printManager.removeEventListener("printtaskrequested", printTaskHandler);

Попробуйте запустить приложение и отправить статью на печать:

image

image

Если вы достаточно наблюдательны, вы должны сразу увидеть возникшую проблему: на печать отправлена только одна страница, причем в том виде, как выглядит страница на экране (с горизонтальной прокруткой).

Аналогично тому, как бы вы решали эту проблему для веба, здесь нам на помощь приходят media-запросы и CSS-правила для печати. (Вы также можете передать на печать специально сгенерированную страницу, фрагмент, iframe и т.п.)

Добавьте в проект новый файл pages\itemDetail\print.css, либо просто запишите приведенный ниже код в css-файл, привязанный к нужной странице (itemDetail.css). Добавьте в него следующие строчки:

@media print {
    /* изменяем отступы страницы*/
    .itemdetailpage.fragment {
        -ms-grid-rows: 40px 1fr;
    }

    /* изменяем расположение заголовка страницы*/
    header[role="banner"] {
        -ms-grid-columns: 40px 1fr !important;
    }

    /* прячем кнопку "назад" */
    .win-backbutton {
        display:none;
    }

    /* изменяем отступы заголовка и шрифт страницы*/
    header[role="banner"] h1.titlearea {
        margin: 0px !important;
        font-size: 1em;
        text-transform: uppercase;
    }
    
    /* изменяем высоту контента и перехлесты для вертикальной прокрутки */
    .itemdetailpage.fragment, .itemdetailpage .content, .itemdetailpage .content article {
        height:auto !important;
        overflow: visible !important;
    }
    
    /* изменяем отступы контента*/    

    .content {
        padding-left: 40px;
    }


    /* убираем многоколоночную верстку */
    .itemdetailpage .content article { 
        column-width: auto;
        width: auto;
        margin-left: 0px;
        margin-top: 40px;
    }

}

Запускаем приложение и пробуем отправить на печать:

image

На этом мы закончим текущую статью.

Проект

Готовый проект на текущей стадии можно скачать тут: http://aka.ms/win8jsreadertemplate_v2.

Далее

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

Предыдущие части

Dec 25

Введение в разработку WinRT-приложений на HTML/JavaScript. Стилизация приложения

Эта статья продолжает серию материалов (первая часть), посвященных азам разработки WinRT-приложений на HTML/JS для Windows 8. Мы последовательно пройдем путь от стартового практически пустого шаблона к полноценному приложению с серверной частью и живыми плитками.

image

Напомню, что в первой статье мы научились добавлять собственные источники данных и остановились на отображении этих данных в сыром виде на страницах приложения.

В данной статье мы займемся внешним видом нашего приложения: добавим картинки, поменяем заголовки, стили, плитки и т.п. Всю работу можно провести в Visual Studio, хотя некоторые вещи удобнее и нагляднее менять в Expression Blend.

XAML/C#. Если вы заинтересованы в разработке с использованием XAML и C#, рекомендую обратить внимание на аналогичную серию статей моего коллеги – Стаса Павлова: Разбираемся с разработкой Windows 8 приложений на XAML/С#, реализуя простой RSS Reader.

Извлечение картинок

Первым делом, давайте попробуем вытащить из постов изображения, если они там, конечно, есть. Для этого откройте файл js\data.js и перейдите к функции getItemsFromRSSFeed, занимающейся разбором отдельных постов.

Добавьте перед строчкой “var postItem = {” следующий код:

            // извлечение ссылки на изображение в посте
            var tempElement = document.createElement("div");
            tempElement.innerHTML = postContent;
            var image = tempElement.querySelector("img");            
            var imglink = (image != null) ?  "url('" + image.src + "')" : "";

Здесь мы создаем временный элемент с контентом поста и извлекаем (querySelector) из него первую же картинку, чтобы использовать ее в качестве фоновой на плитках к постам. В случае, если картинки нет, используем пустую строку, которую передадим в соответствующее правило CSS.

Замечание: здесь мы также можем попасть в ситуацию, когда явной картинки в посте нет, но есть невидимая, применяемая для подсчета числа просмотров. Именно такая ситуация имеет место для блогов, используемых в ходе статьи, однако, эти картинки прозрачные, поэтому в нашем случае это не критично.

Добавьте в объект-описание поста (postItem) следующие строчки в конце, не забыв добавить запятую строчкой выше:

            // ссылка на картинку
            backgroundImage: imglink

Должно получиться примерно вот так:

            var postItem = {
                …
                link: post.querySelector("link").textContent,
                // ссылка на картинку
                backgroundImage: imglink
            };

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

Откройте файл pages\groupedItems\groupedItems.html. Внутри этого файла для описания структуры страницы и связывания данных используются шаблоны. Найдите шаблон элемента, начинающийся строчкой:

<div class="itemtemplate" data-win-control="WinJS.Binding.Template">

Двумя строчками ниже вы увидите описание картинки:

<img class="item-image" src="#" data-win-bind="src: backgroundImage; alt: title" />

Если вы знакомы с основами HTML, вы наверняка уже догадались, почему в отображении ничего не поменялось: формат, который мы использовали выше, рассчитан на использование в CSS, а не явную вставку ссылки на изображение через img.

Замечание: одна из причин, почему в данном случае CSS является предпочтительнее, заключается в более гибких настройках отображения и, в частности, наличие возможности манипуляций с изображением, в том числе с сохранением пропорций.

Удалите эту строчку для вставки изображения.

Поднимитесь на уровень выше и замените строчку

<div class="item">

на следующую:

<div class="item" data-win-bind="style.backgroundImage: backgroundImage">

Здесь мы используем возможности связывания данных (атрибуты data-win-bind) с шаблоном для проекции свойств в данных на атрибуты в элементах html-разметки и DOM.

Попробуйте запустить приложение на отладку:

image

Стало лучше, но по-прежнему не очень. Например, как мы и ожидали, в некоторых постах нет изображений — и вместо них мы получили отсутствие картинок или пустые заглушки. Давайте для таких постов поставим цветной фон. Для этого откройте в той же папке файл groupedItems.css.

Найдите следующую строчку, описывающую отображение отдельного элемента:

.groupeditemspage .groupeditemslist .item {

Добавьте в конце этого CSS-правила описание фона:

       background-color: rgb(0, 204, 255);

Дополнительно можно сделать еще несколько манипуляций над фоновым изображением, например, центрировать его, отменить повторения и задать режим масштабирования:

       background-position: 50% 50%;
       background-repeat: no-repeat;
       background-size: cover;

Запускаем приложение:

image

Теперь давайте перейдем к исправлению отображения текстового содержимого.

Обновление текстовых стилей

Вернитесь к файлу groupedItems.html и описанию шаблона элемента (itemtemplate). В конце элемента вы найдете описание подзаголовка (item-subtitle). В нашем случае никакого подзаголовка нет, зато есть дата, поэтому замените эту строчку:

<h6 class="item-subtitle win-type-ellipsis" data-win-bind="textContent: subtitle"></h6>

на следующие:

                <h6 class="item-date win-type-ellipsis">
                    <span data-win-bind="textContent: day"></span> 
                    <span data-win-bind="textContent: month"></span>
                </h6>

Если вы попробуете сейчас запустить, вы увидите, что дата куда-то съехала и залезла на заголовок.

Чтобы это поправить, перейдите назад к CSS-файлу. Найдите описание подзаголовка:

            .groupeditemspage .groupeditemslist .item .item-overlay .item-subtitle {
                -ms-grid-row: 2;
                width: 220px;
            }

Замените его на соответствующее описание отображение даты:

            .groupeditemspage .groupeditemslist .item .item-overlay .item-date {
                -ms-grid-row: 2;
                width: 220px;
                text-transform: uppercase;
                text-align:right;
            }

В данном случае мы также добавили выравнивание по правому краю и отображение текста в верхнем регистре.

Замечание: обратите внимание, что для позиционирования элементов использует модуль CSS 3 Grid Layout. Мы не будем останавливаться на деталях его использования, но если вы планируете создавать приложения для Windows Store на HTML/JS, рекомендуем хорошо изучить его возможности.

Теперь давайте попробуем сделать заголовок записи побольше и изменить его положение на плитке.

Для начала его необходимо вынести на уровень выше. Перейдите к файлу groupedItems.html и внутри шаблона элемента вынесите описание заголовка на уровень выше, добавив обертку. Должно получиться примерно так:

        <div class="item" data-win-bind="style.backgroundImage: backgroundImage">   
            <div class="item-title-container">
                <h4 class="item-title" data-win-bind="textContent: title"></h4>
            </div>
            <div class="item-overlay">                
                <h6 class="item-date win-type-ellipsis">
                    <span data-win-bind="textContent: day"></span>
                    <span data-win-bind="textContent: month"></span>
                </h6>
            </div>
        </div>

Вернитесь к CSS-файлу, в нем необходимо отобразить изменение иерархии и прописать обновленные стили.

После правила .groupeditemspage .groupeditemslist .item { … } добавьте новое:

        .groupeditemspage .groupeditemslist .item .item-title-container {
            -ms-grid-row: 1;            
            margin: 10px;
            padding: 8px;
            opacity: 0.85;
        }

Сразу после него перенесите находящееся ниже описание заголовка (.item-overlay надо заменить на .item-title-container):

            .groupeditemspage .groupeditemslist .item .item-title-container .item-title {
                overflow: hidden;
                width: 220px;
                display: inline; 
                font-size: 1.6em; 
                line-height: 1.5em;
                font-family: 'Segoe UI Light';
                background: rgb(145, 0, 145); 
                box-shadow: rgb(145, 0, 145) 0 0 0 8px;
            }

Попробуйте запустить проект:

image

Чтобы убрать подложку, оставшуюся от изначального макета, перейдите ниже по коду в CSS-файле к строчкам:

    .groupeditemspage .groupeditemslist .item .item-overlay {
        background: rgba(0,0,0,0.65);
    }

Замените указанный цвет на transparent.

Попробуйте самостоятельно увеличить размер текста у даты и поменять шрифт на “Segoe UI Semibold” и название приложения на другое:

image

В качестве домашнего задания: попробуте поменять внешний вид плиток, чтобы они приняли такой вид (подсказка: вам помогут градиенты):

image

Перейдите внутрь группы и внутрь отдельного блога. Аналогичные действия по изменению внешнего вида необходимо проделать для всех внутренних страниц:

image

image

Переведите приложение в Snapped-режим (если вы работаете в Expression Blend, на вкладке Device можно менять режим отображения). Здесь также нужно поправить стили с учетом выбранной стилистики.

image

Стили для Snap-режима корректируются с помощью Media Queries:

@media screen and (-ms-view-state: snapped) {
      ...
}
Замечание: в данном проекте мы не меняли тему оформления (по умолчанию используется темная). Тема задается в подключаемом из библиотеки WinJS CSS-файле. Вы легко найдете его в заголовке каждой страницы:
 <link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
Попробуйте поменять dark на light, чтобы увидеть разницу. Обратите внимание, что поменялся не только цвет фона, но и цвет текста и других элементов. В нашем случае это может привести к нежелательным последствиям, так как часть использованных цветов рассчитана на темный цвет фона и белый цвет основного текста.

Проект

Готовый проект на текущей стадии можно скачать тут: http://aka.ms/win8jsreadertemplate_v1.

Далее

В следующей части мы займемся улучшением работы с данными и добавлением поддержки нескольких контрактов.

Предыдущие части

Dec 23

Введение в разработку WinRT-приложений на HTML/JavaScript. От шаблона к приложению с данными

Этой статьей мы открываем серию материалов, посвященных азам разработки WinRT-приложений на HTML/JS для Windows 8. Мы последовательно пройдем путь от стартового практически пустого шаблона к полноценному приложению с серверной частью и живыми плитками.

Первая часть посвящена созданию простой версии приложения, читающего внешние данные через RSS-каналы на базе стандартного шаблона. В результате должен получиться работающий прототип приложения, способный показывать новости из нескольких RSS-потоков и отображать их на трех типах страниц: хаб (первая страница), группа и детали.

Создание приложения из шаблона

Откройте Visual Studio 2012, выберите создание нового проекта (File -> New -> Project…). Далее в шаблонах выберите проект на JavaScript -> Windows Store. Укажите, что будете использовать шаблон Grid App.

image

Укажите любое название проекта, например, Reader App.

Изучите структуру проекта:

  1. package.appxmanifest — манифест приложения, описывающий ключевые настройки, используемые возможности, название приложения, плитки и другие параметры;
  2. default.html — формальная стартовая страница приложения;
  3. pages\groupedItems — папка с html-, js- и css-файлами для страницы представления групп контента (подгружается в стартовую страницу);
  4. pages\groupDetail — папка с html-, js- и css-файлами для страницы отображения группы новостей (записей в rss-потоке), соответствующих одному потоку;
  5. pages\itemDetail — папка с html-, js- и css-файлами для страницы отображения каждой из новостей отдельно;
  6. js\data.js — js-файл, описывающий работу с данными (содержит зашитые внутрь демонстрационные данные);
  7. js\default.js — описывает события, необходимые для инициализации приложения;
  8. js\navigator.js — описывает логику переходов между страницами и необходимые для этого события и объекты;
  9. images\logo.png — изображение, используемое для квадратной плитки;
  10. images\smalllogo.png — изображение, используемое при перечислении приложения в операционной системе, например, при поиске или выборе приложений для поиска или общего доступа;
  11. images\splashscreen.png — загрузочное изображение, показываемое при открытии приложения;
  12. images\storelogo.png — изображение, используемое в интерфейсе магазина приложений (Windows Store).

Также по умолчанию к проекту подключена библиотека WinJS, содержащая наборы стилей для темной и светлой тем и вспомогательных функций и объектов на JavaScript.

Попробуйте запустить приложение, нажав F5, зеленую стрелочку или выбрав Debug -> Start Debugging.

image

Изучите работу приложения:

Вернитесь в Visual Studio и остановите отладку (Shift+F5, красный квадратик или выберите в меню Debug -> Stop Debugging).

Замена источников данных

Откройте файл js\data.js. В нем объявлено несколько важных функций и объектов, которые мы также будет использовать.

Первым делом необходимо избавиться от строчек кода, генерирующих примеры данных. Для этого удалите следующие строчки:

  1. Вставка данных в список:
        // You can add data from asynchronous sources whenever it becomes available.
        generateSampleData().forEach(function (item) {
            list.push(item);
        });
    
  2. Генерация примеров данных:
        // Returns an array of sample data that can be added to the application's
        // data list. 
        function generateSampleData() {
            var itemContent = "<p>Curabitur class … ";
            var itemDescription = "Item Description: Pellente…";
            var groupDescription = "Group Description: Lorem …";
            …
            return sampleItems;
        }

Если вы запустите приложение, оно продолжит работать, только в нем будут отсутствовать какие-либо данные. Остановите отладку и вернитесь к файлу data.js. Давайте вкратце пройдемся по его структуре, чтобы были понятны дальнейшие действия:

var list = new WinJS.Binding.List();

Создается список, который будет использоваться для связки данных с отображением. В него мы будем заносить те блоки информации, которые хотим вывести на экран.

var groupedItems = list.createGrouped(
        function groupKeySelector(item) { return item.group.key; },
        function groupDataSelector(item) { return item.group; }
    );

На основании списка создается группированная коллекция, при создании которой с помощью специальных функций указывается, как элементы коллекции разделять на отдельные группы.

WinJS.Namespace.define("Data", {
        items: groupedItems,
        groups: groupedItems.groups,
        getItemReference: getItemReference,
        getItemsFromGroup: getItemsFromGroup,
        resolveGroupReference: resolveGroupReference,
        resolveItemReference: resolveItemReference
    });

Через функцию define в библиотеке WinJS (Namespace) прописывается глобальный объект Data, который будет доступен из других частей программы для работы с нашими данными. Внутри объекта прописываются ссылки на группированную коллекцию, список групп внутри нее и ряд функций, описанных в файле data.js и используемых для работы с коллекцией и извлечения данных.

Оставшиеся 4 функции (getItemReference, getItemsFromGroup, resolveGroupReference и resolveItemReference) используются для сравнения объектов, извлечения подмножеств элементов, принадлежащих одной группе, определения группы по ключу и элемента по набору уникальных идентификаторов.

Теперь самое время приступить к добавлению наших собственных данных. В качестве источников мы будет использовать внешние RSS-потоки.

Важно: в рамках данной статьи в целях упрощения кода для получения данных мы будет использовать только RSS-потоки, однако, добавление поддержки Atom-потоков не должно составить труда, так как они имеют схожую структуру и ключевая разница будет в адресации искомых полей данных.

Вернитесь к началу файла и после строчки “use strict” опишите блоги, информацию из которых вы будете выводить:

        var blogs = [
            {
                key: "ABlogging",
                url: "http://blogs.windows.com/windows/b/bloggingwindows/rss.aspx",
                title: 'Blogging Windows', rsstitle: 'tbd', updated: 'tbd',
                dataPromise: null
            },
            {
                key: "BExperience",
                url: 'http://blogs.windows.com/windows/b/windowsexperience/rss.aspx',
                title: 'Windows Experience', rsstitle: 'tbd', updated: 'tbd',
                dataPromise: null
            },
            {
                key: "CExtreme",
                url: 'http://blogs.windows.com/windows/b/extremewindows/rss.aspx',
                title: 'Extreme Windows', rsstitle: 'tbd', updated: 'tbd',
                dataPromise: null
            }];

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

Чтобы превратить ссылки в данные на компьютере, информацию по ним необходимо загрузить. Для этого после строчки var list = new WinJS.Binding.List(); добавьте новую функцию getBlogPosts, которая как раз будет заниматься загрузкой:

function getBlogPosts(postsList) {
        blogs.forEach(function (feed) {
            // Создание Promise
            feed.dataPromise = WinJS.xhr( { url: feed.url } ).then(
                function (response) {
                    if (response) {
                        var syndicationXML = response.responseXML || (new DOMParser()).parseFromString(response.responseText, "text/xml");
                        processRSSFeed(syndicationXML, feed, postsList);
                    }
                }
            );
        });

        return postsList;
    }

В данной функции мы в цикле проходимся по всем блогам, для каждой ссылки через функцию WinJS.xhr создаем асинхронную Promise-обертку вокруг XMLHttpRequest-запроса и после получения результата (then) передаем полученный ответ на обработку в функцию processRSSFeed, которую мы опишем ниже.

Замечание: для используемых нами блогов нет необходимости в дополнительной проверке, что мы получили ответ в виде XML (наличие responseXML), однако, в общем случае это неверно: некоторые блоги из-за неверных настроек сервера/движка отдают RSS-поток как текстовое содержимое, которое необходимо дополнительно обрабатывать, если мы хотим работать с ним как с XML-файлом.

Для обработки потока добавьте нижу еще одну функцию — processRSSFeed:

    function processRSSFeed(articleSyndication, feed, postsList) {
        // Название блога
        feed.rsstitle = articleSyndication.querySelector("rss > channel > title").textContent;
        // Используем дату публикации последнего поста как дату обновления
        var published = articleSyndication.querySelector("rss > channel > item > pubDate").textContent;

        // Преобразуем дату в удобрый формат
        var date = new Date(published);
        var dateFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter(
           "day month.abbreviated year.full");
        var blogDate = dateFmt.format(date);
        feed.updated = "Обновление: " + blogDate;

        // Обработка постов
        getItemsFromRSSFeed(articleSyndication, feed, postsList);
    }

В данной функции мы пользуемся тем фактом, что на входе имеем XML-файл с известной структурой (RSS 2.0), по которому можно перемещаться используя DOM-модель, в частности, функцию querySelector, используя которую, можно вытаскивать из полученного документа необходимые данные.

Полученное текстовое значение последней даты обновления мы преобразуем в нужный формат, используя функции глобализации, доступные через API WinRT.

В конце мы передаем документ на дальнейшую обработку в функцию getItemsFromRSSFeed, которая выделяет отдельные посты и заносит их в коллекцию posts.

Добавьте ниже следующую функцию:

 function getItemsFromRSSFeed(articleSyndication, feed, postsList) {
        var posts = articleSyndication.querySelectorAll("item");

        // Цикл по каждому посту в потоке
        var length = posts.length;
        for (var postIndex = 0; postIndex < length; postIndex++) {
            var post = posts[postIndex];
            // форматирование даты
            var postPublished = post.querySelector("pubDate").textContent;
            var postDate = new Date(postPublished);
            var monthFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter("month.abbreviated");
            var dayFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter("day");
            var yearFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter("year.full");
            var timeFmt = new Windows.Globalization.DateTimeFormatting.DateTimeFormatter("shorttime");
            
            var postContent = toStaticHTML(post.querySelector("description").textContent);
            
            var postItem = {
                index: postIndex,
                group: feed,
                // заголовок поста
                title: post.querySelector("title").textContent,
                // дата и отдельные компоненты
                postDate: postDate,
                month: monthFmt.format(postDate).toUpperCase(),
                day: dayFmt.format(postDate),
                year: yearFmt.format(postDate),
                time: timeFmt.format(postDate),
                // содержимое поста
                content: postContent,
                // ссылка на пост
                link: post.querySelector("link").textContent
            };

            postsList.push(postItem);
        }
    }

В данной функции мы проходимся в цикле по всем постам в полученном потоке, выбирая из XML-описания нужные поля (заголовок, дату публикации, контент и т.п.), после чего собираем нужную информацию в один объект (postItem), который добавляем в список постов.

Обратите внимание на форматирование дат и привязку каждого поста к соответствующей группе. Также заметьте, что для повышения безопасности полученное содержимое поста мы приводим к статическому виду с помощью функции toStaticHTML.

Замечание: в общем случае работы с RSS-потоками из неконтролируемых источников нужно внимательно контролировать (и исследовать) получаемые данные. На практике часть данных по искомым полям может отсутствовать, поэтому прежде, чем извлекать текстовое содержимое (textContent) необходимо убеждаться, что предыдущая операция вернула не null-значение. В некоторых случаях полное содержимое поста может скрываться за элементами encoded или full-text или и вовсе отсутствовать. Также известны случаи, когда сервер отдает дату в неправильном формате, в результате чего стандартный парсер выдает исключение.

Добавьте ниже следующую строчку, чтобы запустить считывание блогов:

list = getBlogPosts(list);

Запустите приложение на отладку:

image

Как видите, оно уже использует наши новые данные, однако, есть несколько «проблемных» зон, которые необходимо поправить:

Этими задачами мы займемся в следующих статьях.

Nov 19

Kinected Browser

Если у вас есть Kinect for Windows, теперь вы можете попробовать управлять жестами поведением объектов в браузере (Internet Explorer)!

Плагин пробрасывает API для работы с Kinect в JavaScript:

  1. Scriptable high level DOM events for joint activity such as leftHandOver, rightKneeMove, etc.
  2. Access to full skeleton tracking
  3. Support for multiple and custom methods for mapping physical space into the browser
  4. Access to the depth stream with built in methods for drawing to HTML5 Canvas

Экспериментальный плагин: http://research.microsoft.com/en-us/projects/kinectedbrowser/

Release Notes с руководством по внедрению: http://research.microsoft.com/en-us/projects/kinectedbrowser/releasenotes.aspx

Научная статья: http://research.microsoft.com/apps/pubs/default.aspx?id=172689

Images in a responsive web -

Tyson Matanich пишет о том, как делались отзывчивые картинки для нового сайта Microsoft.com.

Jul 27

FoxIE - о браузерах, веб-стандартах и инструментах -

В новой серии роликов на Channel 9 евангелисты Christian Heilmann из Mozilla и Rey Bango из Microsoft рассказывают о современных браузерах, веб-стандартах и инструментах для веб-разработки.

May 25

Плакат: современные веб-стандарты

Комментарии приветствуются: что добавить, что поправить?

pdf: http://narod.ru/disk/54283069001.c5cab3816eeff2fe004a73246deb668b/Short%20-%20Modern%20Web%20Standards.pdf.html

png: http://img-fotki.yandex.ru/get/6313/25193168.13/0_59b32_81ac24b4_orig    

May 09

(Source: browseryoulovedtohate)

The top 8 web standards myths debunked -

Lea Verou рассказывает о мифах вокруг веб-стандартов и рабочих групп W3C.

May 02

Введение в CSS3 Multicolumn. Работаем с колонками

Введение

apples

Как расположить текст на странице в несколько колонок? И можно ли это делать автоматически? Наверняка, многие из тех из вас, кто занимается или занимался раньше веб-разработкой, сталкивались с такой задачей — и часто упирались в сложные решения, требующие хитрых стилей, либо применения дополнительных библиотек на JavaScript (см. например Columnizer-плагин для jQuery).

Многоколоночная верстка контента (не путать с задачей общей многоколоночной верстки страницы, которая скорее ближе к проблеме расположения блоков по сетке) долго пробивала себе дорогу в мире веб-стандартов и, наконец-то, не просто достигла статуса Candidate Recommendation в виде соответствующего модуля CSS3 Multi-column Layout, но и получила достаточно широкую поддержку в браузерах: где-то с префиксами (-moz- или -webkit-) и где-то в актуальных (Opera 11.1+) и планируемых версиях (IE10+), причем сразу без префиксов.

В случае Internet Explorer 10 это автоматически означает возможность использования CSS3 Multi-column при разработке приложений в стиле Metro для Windows 8 с использованием HTML/CSS/JS.В рамках статьи я не буду использовать браузерные префиксы, чтобы на запутывать код, но при реальном использовании не забудьте добавить поддержку префиксов для Firefox, Safari и Chrome.

Сразу отмечу еще две немаловажные детали.

Во-первых, в большинстве случаев применение многоколоночной верстки для текста можно рассматривать как развитие отображения контента сайта по пути Progressive Enhancement, в рамках которого пользователи более современных сайтов будут получать больше плюшек:

progressive enhacement

Во-вторых, многоколоночное отображение хорошо сочетается с возможностями Media Queries (и идеями отзывчивого дизайна, Responsive Design), например, при разных размерах экрана можно форматировать текст в разное количество колонок:

responsive design

И последнее, что я хотел бы отметить во введении, чтобы на этом не останавливаться далее и со спокойной совестью перейти к техническим деталям: при использовании многоколоночной верстки текста надо помнить, что подобное расположение предполагает и определенный порядок чтения (для европейских языков слева направо). Поэтому важно, чтобы для перевода взгляда от одной колонки к другой было необходимо совершать как можно меньше дополнительных действий, особенно это касается вертикальных скроллов:

multicolumn layout vs vertical scroll

В этом смысле горизонтальная природа колонок лучше сочетается с горизонтальным скроллом (как это, используется во многих приложениях для Win8 — например, это хорошо видно по приложению USA Today):

multicolumn layout plus horizontal scroll

В общем, колонки — это прекрасно, но не забывайте об удобстве пользователей. А теперь в бой!

Колонки

Итак, у нас есть текст (контент), который мы хотим разместить в несколько колонок. С чего начать?

plain text

Чтобы превратить такой элемент в многоколоночный нужно через стили в CSS выставить одно из свойств: column-width или column-count в значение, отличное от auto. Например, чтобы разбить текст на две колонки, достаточно сделать так:

article {
    column-count: 2;
}

Все остальное сделает браузер:

2 columns

Альтернативное свойство — column-width — позволяет задать оптимальную ширину колонок:

article {
    column-width: 150px;
}

3 columns

При этом браузер сам разбивает контент на нужное количество колонок, чтобы заполнить внешний контейнер, подстраиваясь под указанную ширину колонок. Важный момент заключается в том, что реальная ширина может отличаться от заданной в большую или меньшую сторону: на картинке выше серая полоска имеет как раз ширину в 150px — и, как видно, она меньше, чем реальная ширина колонки.

Такое поведение определено спецификацией и позволяет (автоматически) добиться большей гибкости при разработке адаптивной разметки:

adaptive column width

Например, если у вас контейнер шириной 100px и вы задали колонки шириной 45px, то браузер посчитает, что влезет только две колонки, а чтобы заполнить все место, увеличит размер каждой до 50px. (Здесь также учитывается отступ между колонками, о чем будет рассказано в следующем разделе.)

В определенном смысле, это можно рассматривать как альтернативу указанию с помощью Media Queries разного количества колонок в зависимости от размера окна и с автоматическим рассчетом ширины колонок:

@media (min-width:400px) {
    article {
        column-count: 2;
    }
}
@media (min-width:600px) {
    article {
        column-count: 3;
    }
}
...

Я второй раз говорю про альтернативу — и вот почему.

count vs. width

Как уже должно быть понятно из описания выше, спецификация дает два способа для задания количества и ширины колонок (кстати, у всех колонок она одинаковая!):

В большинстве случаев вы будете использовать либо одно, либо другое, оперируя соответственно числами, либо длинами. Для упрощения записи есть такое короткое свойство columns, реагирующее на формат указываемых данных:

columns: 12em;      /* column-width: 12em; column-count: auto; */
columns: 2;         /* column-width: auto; column-count: 2; */
columns: auto;      /* column-width: auto; column-count: auto; */
columns: auto 12em; /* column-width: 12em; column-count: auto; */
columns: 2 auto;    /* column-width: auto; column-count: 2; */

Что будет, если указать и количество колонок, и оптимальную ширину? Согласно спецификации, в этом случае column-count определяет максимальное количество колонок:

article {
    columns: 150px 3;  /* column-width: 150px; column-count: 3; */          
}

limited to 3 columns

В спецификации вы также найдете псевдо-код, описывающий алгоритм рассчета количества колонок и их ширины.

Отступы и разделительные линии

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

Чтобы изменить отступ между колонками, определено свойство column-gap. Чтобы визуально обозначить раздел между колонками, введено еще одно свойство — column-rule-*:

column gap and rule

column-gap

column-gap позволяет указать ширину пространства между колонками. Свойство принимает в качестве значения длину, либо определяемое браузером значение normal (спецификация рекомендует использовать 1em), являющееся также значением по умолчанию:

article {
    columns: 3;  
    columns-gap: 3em;          
}

3em gap

Важно учитывать, что размер отступа между колонками используется при расчете ширины колонок и их количества. Например, если указано только количество колонок (как в примере выше), то ширина рассчитывается так:

W = ((available-width + column-gap) / N) - column-gap;

column-rule

Если для обозначения колонок свободного пространства недостаточно, можно использовать свойства column-rule-*, добавляющие линию между колонками. По своему поведению и заданию аналогичные свойствам border-*:

Как и в случае с границами блоков, есть краткая форма записи — просто column-rule:

article {
    columns: 3;  
    column-rule: 3px dotted CornflowerBlue;         
}

dotted rule

Важный момент: разделительная линия отрисовывается ровно посередине отступа между колонками, при этом не занимая места. Если вдруг разделительная линия оказывается больше по ширине, она начинает пересекаться с контентом:

article {
    columns: 3;  
    column-rule: 5em groove SkyBlue;      
}

rule overflow

Причем отрисовывается разделитель сразу после фона (background) контейнера.

Разрывы

Распределение контента по колонкам — еще одна интересная задача, требующая иногда тонкого управления. По умолчанию, контент — это сплошная “масса”, весьма прямолинейным образом, перетекающая из одной колонки в другую. Но что делать, если, скажем, я хочу быть уверенным, что в конце колонки у меня не повиснет одинокий заголовок? Или если мое визуально-эстетическое чувство требует, чтобы цитата не разрывалась на несколько колонок или даже больше: единолично занимала целиком всю колонку?

Для решения всех этих задач есть специальные свойства. Знакомьтесь:

Если вы знакомы с аналогичными свойствами, отвечающими за разбивку контента на страницы (page-break-*), то данные свойства для колонок ведут себя схожим образом: используют те же значения плюс несколько дополнительных (отмечены курсивом):

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

article dialog {
    break-inside:avoid;
    break-before: column;
    break-after: column;
    display:block;
}

column breaks

Важный момент: на сегодня управление разрывами в колонках поддерживается только в Opera 11.10+, — что не удивительно, учитывая, что редактор спецификации Håkon Wium Lie, — и IE10. Мои эксперименты со свежей версией Оперы и предварительной публичной версией IE10 показывают, что местами имеющиеся реализации отличаются друг от друга. Однако тут я затрудняюсь ответить, какой браузер ведет себя “правильней”, так как спецификация хотя и содержит отдельный раздел, посвященный переполнению (overflow), все же оставляет некоторые нюансы на усмотрение браузера (например, позволяет появление дополнительных экстра-колонок при явном указании разрывов).

Растягивание на несколько колонок

Теперь, когда мы научились создавать колонки и немного управлять поведением контента, давайте научимся еще одному трюку — растягиванию контента на несколько колонок. Для этого есть специальное свойство: column-span, принимающее значения none и all.

Нас интересует второе значение. Оно выдергивает блок контента из общего многоколоночного потока и растягивает его на все колонки. При этом контент до этого элемента и контент после автоматически балансируются на все имеющиеся колонки.

article dialog.big {
    column-span:all;
    display:block;
    font-size:1.3em;
    margin:20px 0;
}

column span

В данном случае написанная крупным фраза из диалога растянута на все колонки. Обратите внимание на порядок следования текста: верхняя левая колонка, верхняя правая, фраза диалога, нижняя левая и далее нижняя правая.

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

Растягивание элементов на сегодня все еще не поддерживается в Firefox.

Заполнение

И последняя деталь, которой вы, наверняка, уже должны были озадачиться: а как, собственно говоря, браузер решает, как ему заполнять колонки?

Для ответа на этот вопрос спецификация вводит свойство column-fill. Заполнять можно сбалансированно (balance), — именно так делается по умолчанию, — стараясь выдержать одинаковую высоту колонок; либо автоматически (auto), заполняя колонки последовательно.

Сравните, вот так браузер балансирует по умолчанию:

article  {
    columns: 2;
    /*column-fill:balance;*/
    height: 250px;
}

column fill balance

А вот так в автоматическом последовательном режиме:

article  {
    columns: 2;
    column-fill:auto;
    height: 250px;
}

column fill auto

Управление заполнением на сегодня поддерживают только Internet Explorer и Opera.

Итоги

Прежде всего, продолжение повести А.П. Чехова “За яблочки” можно найти в Викитеке.

По существу дела, следя за развитием веб-стандартов, в том числе по некоторым моим статьям про CSS3 (см. например, статью про CSS3 Grid Layout), я надеюсь, вы с не меньшим вдохновением смотрите на открывающиеся перед веб-разработчиками возможностями. Адаптивные, гибкие и мощные средства для управления размещением контента все ближе и доступнее. А решение сложных задач — все проще.

Интерактив

Поиграться с работой CSS3 Multi-column можно на ietestdrive.com:

hands on

Пробуйте, эксперементируйте. Сообщайте разработчикам браузеров о багах. И не забывайте продумать, что увидят пользователи старых (и вроде бы современных, но все еще не полностью поддерживающих стандарт) браузеров — например, можно использовать плагин для jQuery Columnizer. Помните об адаптивности и зрителях маленьких и больших экранов.