Опрос
Вы участвуете в программе Windows Insider?
Популярные новости
Обсуждаемые новости

Начала Metro-программирования: создание компонентов WinRT (ч.2)

Напечатать страницу
20.12.2012 17:10 | dronov_va

Вторая часть статьи, посвящённой создания компонентов WinRT.


5. Создание более сложных компонентов
Что ж, созданный нами простейший компонент работает. Давайте добавим ему функциональности.


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

Свойства и методы делятся на две разновидности:

  • Свойства и методы экземпляра объекта. Они вызываются у экземпляров данного объекта, хранят значения, характерные для конкретных экземпляров, и выполняют действия над конкретными экземплярами. К таким свойствам и методам можно отнести свойство innerHTML, хранящее HTML-код содержимого конкретного элемента интерфейса, и метод get, возвращающий элемент конкретного экземпляра объекта-коллекции WinJS.Utilities.QueryCollection.
  • Свойства и методы объекта. Они вызываются не у экземпляров, а у самого объекта, хранят значения, специфичные для всех экземпляров данного объекта, и выполняют какие-либо специальные действия. Типичный пример - метод setInnerHTML объекта WinJS.Utilities, который вызывается у самого этого объекта.


Давайте создадим у раскрывающейся панели такой набор свойств экземпляра:

  • isOpened - хранит значение true (панель развёрнута) или false (панель свёрнута);
  • headerHTML - хранит HTML-код содержимого блока заголовка в виде строки;
  • contentHTML - хранит HTML-код содержимого блока содержимого в виде строки.


Дополнительно для своего удобства создадим ещё два свойства объекта: expandIndicator и collapseIndicator. Первое свойство будет хранить символ-индикатор, обозначающий развёрнутое состояние панели, а второе - символ, обозначающий свёрнутое состояние. Сделаем оба свойства доступными только для чтения (как это делается, мы узнаем чуть позже).

А набор методов нашего компонента будет таким:

  • expand - разворачивание панели;
  • collapse - сворачивание панели;
  • toggle - переключение состояния панели (разворачивание свёрнутой или сворачивание развёрнутой).


Все эти методы не будут принимать параметров и не будут возвращать результата.

Разбирая в параграфе 4.1 метод define объекта WinJS.Class, мы узнали, что он принимает три параметра, но рассмотрели только первый - функцию-конструктор. Настала пора познакомиться с остальными.

Второй параметр задаёт набор элементов - свойств и методов - экземпляров данного объекта, а третий параметр - набор элементов самого объекта. Значения обоих этих параметров должны представлять собой экземпляры объекта Object с наборами свойств; имена этих свойств станут именами свойств и методов, под которыми они будут доступны "извне", а значения - реализациями этимх свойств и методов.

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

Сначала узнаем, чем настоящее свойство отличается от поля. Поле - это просто переменная, принадлежащая объекту; при получении его значения или занесении в него нового никаких дополнительных действий выполнено не будет.

Напротив, свойство - это не переменная, а набор из двух функций.

  • Get-функция, или геттер, выполняется при получении значения свойства. Она не принимает параметров и в качестве результата возвращает искомое значение. Обычно такая функция извлекает значение из специально созданного частного поля; также она может вычислять это значение.
  • Set-функция, или сеттер, выполняется при занесении в свойство нового значения. Они принимает единственный параметр - новое значение свойства - и не возвращает результата. Обычно такая функция заносит новое значение в специально отведённое для этого частное поле; также она может выполнить некоторые дополнительные действия, например, вывод нового значения на экран или присвоение его какому-либо свойству элемента интерфейса.


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

Определение свойства должно представлять собой экземпляр объекта Object с двумя свойствами. Свойство get должно иметь в качестве значения get-функцию, а свойство set - set-функцияю.

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

(function () {
  "use strict";

  var spoilerClass = WinJS.Class.define(
    function (element) {
      this.element = element;
      this.element.winControl = this;

      this._isOpened = true;
      this._headerHTML = "";
      this.indicator = null;

      var cDivs = WinJS.Utilities.query("div", this.element);
      this.header = cDivs.get(0);
      this.content = cDivs.get(1);

      this.headerHTML = this.header.innerHTML;

      this.isOpened = false;

      var that = this;

      this.header.addEventListener("click",
        function ()
          {
            that.toggle();
          }
      );
    },
    {
      isOpened:
        {
          get: function ()
            {
              return this._isOpened;
            },
          set: function (value)
            {
              if (value != this._isOpened) {
                if (value) {
                  this.content.style.display = "block";
                  WinJS.Utilities.setInnerHTML(this.indicator,
                    TheVista.UI.Spoiler.collapseIndicator);
                } else {
                  this.content.style.display = "none";
                  WinJS.Utilities.setInnerHTML(this.indicator,
                    TheVista.UI.Spoiler.expandIndicator);
                }
                this._isOpened = value;
              }
            }
        },
      headerHTML:
        {
          get: function ()
            {
              return this._headerHTML;
            },
          set: function (value)
            {
              if (value != this._headerHTML) {
                WinJS.Utilities.setInnerHTML(this.header, "[<span>" +
                  (this.isOpened ? TheVista.UI.Spoiler.collapseIndicator :
                  TheVista.UI.Spoiler.expandIndicator) + "</span>]&nbsp;" + value);
                this.indicator = WinJS.Utilities.query("span", this.header).get(0);
                this._headerHTML = value;
              }
            }
        },
      contentHTML:
        {
          get: function ()
            {
              return this.content.innerHTML;
            },
          set: function (value)
            {
              if (value != this.content.innerHTML) {
                WinJS.Utilities.setInnerHTML(this.content, value);
              }
            }
        },
      expand: function ()
        {
          this.isOpened = true;
        },
      collapse: function ()
        {
          this.isOpened = false;
        },
      toggle: function ()
        {
          this.isOpened = !this.isOpened;
        },
    },
    {
      expandIndicator:
        {
          get: function ()
            {
              return "+";
            }
        },
      collapseIndicator:
        {
          get: function ()
            {
              return "-";
            }
        }
    }
  );

  WinJS.Namespace.define("TheVista.UI",
    {
      Spoiler: spoilerClass
    }
  );
})();


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

Прежде всего, в коде конструктора мы объявили поля _isOpened (здесь хранится значение свойства isOpened), _headerHTML (значение свойства headerHTML) и indicator. Это нужно для того, чтобы код get- и set-функций соответствующих свойств смог без проблем получить к ним доступ.

Код, вставляющий в начало содержимого блока заголовка индикатор состояния панели, мы поместили в тело set-функции свойства headerHTML. Там же находится код, получающий доступ к встроенному контейнеру <span>, в котором находится сам символ-индикатор, и присваивающий его полю indicator. Это позволит нам при любом программном изменении содержимого блока заголовка сохранить работоспособность компонента.

Код, разворачивающий и сворачивающий панель, мы поместили в тело set-функции свойства isOpened. Благодаря этому весь код метода expand, разворачивающего панель, сводится к присваиванию свойству isOpened значения true. То же самое можно сказать и о методах collapse и toggle.

В теле конструктора мы извлекаем содержимое блока заголовка и тотчас присваиваем его свойству headerHTML. Set-функция этого свойства сама вставит в блок заголовка индикатор состояния панели.

Далее мы присваиваем свойству isOpened значение false. Set-функция данного свойства свернёт панель и установит соответствующий символ-индикатор.

А обработчик события click, что мы привязываем в конструкторе к блоку заголовка, выполняет одну-единственную задачу - вызывает метод toggle.


5.2. Создание событий
Часто бывает необходимо вклиниться в работу компонента и добавить ему дополнительную логику. Мы давно знаем, как это делается, - с помощью событий и их обработчиков.

Давайте добавим нашему компоненту поддержку событий. Таких событий будет два:

  • beforetoggle - возникает перед изменением состояния компонента - разворачиванием или сворачиванием - и позволяет предотвратить это изменение;
  • aftertoggle - возникает после изменения состояния компонента.


Каждый обработчик события получает в качестве единственного параметра экземпляр особого объекта, порождённого от "общего" объекта Event и хранящего сведения о возникшем событии. Объект, хранящий светения о событи компонента, поддерживает свойство detail. В нём хранится экземпляр объекта Object, свойства которого содержат дополнительные сведения о возникшем событии; если таких сведений нет, свойство detail хранит значение null.

Пусть в обработчик события beforetoggle через свойство detail передаётся дополнительный параметр isOpening. Значение true обозначает, что компонент будет развёрнут, а значение false - свёрнут.

Ещё объекты, представляющие события, поддерживает не принимающий параметров метод preventDefault. Он отменяет поведение компонента по умолчанию.

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

Чтобы дать компоненту поддержку каких-либо событий, мы должны добавить к его реализации методы addEventListener (выполняет привязку обработчика к событию), removeEventListener (удаляет привязку обработчика к событию) и dispatchEvent (инициирует возникновение события и выполнение его обработчиков, если такие есть). Конечно, можно написать реализующий их код вручную... Но есть способ лучше.

Платформа WinRT поддерживает так называемые миксы (mixins). Их можно рассматривать как обычные объекты, чьи поля, свойства и методы можно добавить к любому объекту. Миксы - неплохое средство добавить сразу нескольким разнородным объектам какую-либо одинаковую функциональность.

Все перечисленные ранее методы поддерживаются миксом WinJS.Utilities.eventMixin. Именно его мы и добавим к созданному нами компоненту.

Добавление миксов к объекту выполняет метод mix объекта WinJS.Class. Он должен принимать, по меньшей мере, два параметра. Первым параметром передаётся объект, которому следует добавить один или большее количество миксов. Следующими параметрами передаются сами добавляемые к объекту миксы.

Метод mix возвращает в качестве результата объект с уже добавленными миксами. Обычно этот результат игнорируют.

WinJS.Class.mix(spoilerClass, WinJS.Utilities.eventMixin);


Это выражение добавит нашему объекту микс WinJS.Utilities.eventMixin, содержащий реализацию поддержки событий.

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

Такой микс создаётся вызовом метода createEventProperties объекта WinJS.Utilities. Данный метод принимает произвольное количество параметров, каждый из которых должен представлять собой имя создаваемого события в виде строки. А возвращает он полностью созданный микс...

...который мы, опять же, добавим к компоненту с помощью метода mix объекта WinJS.Class.

WinJS.Class.mix(spoilerClass, WinJS.Utilities.createEventProperties("beforetoggle", "aftertoggle"));


Теперь наш компонент поддерживает события beforetoggle и aftertoggle.

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

dispatchEvent(
  <имя события>[,
  <дополнительные параметры события>]
)

раметры события>]
)[/code]
Первым, обязательным, параметром передаётся имя события, возникновение которого следует инициировать, в виде строки. Вторым, необязательным, параметром передаётся список дополнительных параметров этого события в виде экземпляра объекта Object. Этот экземпляр объекта можно будет потом получить через свойство detail объекта, представляющего событие.

Метод dispatchEvent возвращает в качестве результата значение true, если в обработчике события был вызван метод preventDefault, и false в противном случае. Таким образом, мы можем отследить, отменил ли разработчик, использующий наш компонент, его поведение по умолчанию, и заблокировать в этом случае изменение его состояния.

Настала пора всё-таки добавить нашему компоненту поддержку событий. Перепишем код set-функции свойства isOpened, чтобы он выглядел следующим образом:

[code]set: function (value)
{
if (value != this._isOpened) {
if (!(this.dispatchEvent("beforetoggle", {isOpening: value}))) {
if (value) {
this.content.style.display = "block";
WinJS.Utilities.setInnerHTML(this.indicator,
TheVista.UI.Spoiler.collapseIndicator);
} else {
this.content.style.display = "none";
WinJS.Utilities.setInnerHTML(this.indicator,
TheVista.UI.Spoiler.expandIndicator);
}
this._isOpened = value;
this.dispatchEvent("aftertoggle");
}
}
}[/code]
И не забудем поставить перед вызовом метода define объекта WinJS.Namespace код, добавляющий к нашему объекту необходимые миксы:

[code]WinJS.Class.mix(spoilerClass,
WinJS.Utilities.eventMixin,
WinJS.Utilities.createEventProperties("beforetoggle", "aftertoggle")
);[/code]
Вот и всё. Теперь наш компонент получил поддержку событий.

5.3. Создание настраиваемого компонента
Все компоненты WinRT, за исключением самых простых, поддерживают возможность декларативной настройки - указания изначальных значений их свойств прямо в теге, что создаёт элемент-основу. Выполняется это с помощью атрибута тега data-win-options, в качестве значения которого указывается экземпляр объекта Object; свойства этого экземпляра объекта соответствуют свойствам компонента, а их значения станут значениями свойств компонента.

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

Ещё в параграфе 4.2 мы узнали, что конструктор объекта может принимать два параметра. Первый параметр - это элемент-основа компонента, а второй - список его параметров, то есть изначальных значений его свойств. Фактически вторым параметром конструктору передаётся экземпляр объекта Object, что указан в атрибуте тега data-win-options. Обычно второй параметр носит имя options.

В теле конструктора нам следует каким-то образом просмотреть содержимое экземпляра объекта Object, переданного вторым параметром, и присвоить значения его свойств свойствам компонента. Это можно сделать, вызвав метод setOptions объекта WinJS.UI. (Этот объект предоставляет набор методов для работы с компонентами и является частью WinRT.)

[code]WinJS.UI.setOptions(
<экземпляр объекта-компонента>,
<экземпляр объекта Object, хранящий параметры>
)[/code]
Первым параметром этому методу передаётся экземпляр объекта-компонента, созданный конструктором и хранящийся в переменной this. Вторым параметром передаётся экземпляр объекта Object, хранящий параметры компонента и полученный конструктором в его втором параметре.

Метод setOptions не возвращает результата.

Чтобы дать своему компоненту поддержку настроек, нам достаточно добавить в список параметров конструктора ещё один - options - и вставить в код конструктора одно-единственное выражение (выделено комментариями):

[code]function (element, options) {
. . .
this.isOpened = false;

//Вставленный код - начало

WinJS.UI.setOptions(this, options);

//Вставленный код - конец

var that = this;
. . .
},[/code]
5.4. Создание адаптирующегося компонента
Наш компонент становится всё более и более развитым. Он получил поддержку свойств, методов, событий и настроек и теперь вполне может использоваться в приложениях - как наших собственных, так и чужих.

Однако перед тем, как отдавать компонент в пользование сторонним разработчикам (делать его отчуждаемым), его следует ещё немного доработать. И вот почему...

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

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

Нам надо как-то этого избежать.

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

  • Если разработчик, использующий компонент, поместит содержимое раскрывающейся панели прямо в элемент-основу, это содержимое будет автоматически перемещено во вновь созданный блок, который и станет блоком содержимого. Также будет создан блок заголовка, который получит содержимое по умолчанию, скажем, строку "Спойлер".
  • Если разработчик создаст только блок содержимого, блок заголовка будет создан автоматически и также получит содержимое по умолчанию - строку "Спойлер".


Так мы сделаем компонент адаптирующимся.

Выполняющие всё это выражения мы поместим в тело конструктора (вставленный код выделен комментариями):

[code]function (element, option) {
. . .

this._isOpened = true;
this._headerHTML = "";
this.indicator = null;

//Вставленный код - начало

switch (WinJS.Utilities.query("div", this.element).length) {
case 0:
WinJS.Utilities.setInnerHTML(this.element, "<div>Спойлер</div><div>" +
this.element.innerHTML + "</div>");
break;
case 1:
WinJS.Utilities.insertAdjacentHTML(this.element, "afterbegin", "<div>Спойлер</div>");
}

//Вставленный код - конец

var cDivs = WinJS.Utilities.query("div", this.element);
this.header = cDivs.get(0);
this.content = cDivs.get(1);

. . .
},[/code]
Рассмотрим вновь созданный код.

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

Как найти все нужные нам элементы интерфейса, мы уже знаем, - с помощью метода query объекта WinJS.Utilities. Возвращённый ей экземпляр объекта-коллекции WinJS.Utilities.QueryCollection поддерживает свойство length, хранящее количество найденных элементов. Им-то мы и воспользуемся.

Внимание!
В документации по объекту WinJS.Utilities.QueryCollection не указано, что он поддерживает свойство length. Однако свойство это присутствует и исправно работает.

Если какие-либо блоки внутри внешнего отсутствуют вообще, мы помещаем туда блок заголовка со строкой "Спойлер" и блок содержимого, внутрь которого помещаем старое содержимое внешнего блока. Для этого мы используем давно знакомый нам метод setInnerHTML объекта WinJS.Utilities.

Если внутри внешнего блока присутствует всего один блок, мы вставляем перед ним блок заголовка со строкой "Спойлер". В этом нам поможет метод insertAdjacentHTML объекта WinJS.Utilities.

[code]WinJS.Utilities.insertAdjacentHTML(
<элемент интерфейса>,
<позиция вставляемого кода>,
<вставляемый код>
);[/code]
Первым параметром этому методу передаётся элемент интерфейса, в который или рядом с которым нужно вставить HTML-код.

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

  • "beforebegin" - перед открывающим тегом элемента (то есть перед самим элементом);
  • "afterbegin" - после открывающего тега элемента (в начале его содержимого);
  • "beforeend" - перед закрывающим тегом элемента (в конце его содержимого);
  • "afterend" - после закрывающего тега элемента (после самого элемента).


Наконец, третьим параметром передаётся сам вставляемый HTML-код в виде строки.

Метод insertAdjacentHTML не возвращает результата.

Теперь наш компонент сам сможет исправить некоторые ошибки стороннего разработчика.


Дополнительная литература


Окончание следует...


dronov_va, TheVista.Ru Team
Декабрь 2012

Комментарии

Комментариев нет...
Для возможности комментировать войдите в 1 клик через

По теме

Акции MSFT
420.55 0.00
Акции торгуются с 17:30 до 00:00 по Москве
Все права принадлежат © ms insider @thevista.ru, 2022
Сайт является источником уникальной информации о семействе операционных систем Windows и других продуктах Microsoft. Перепечатка материалов возможна только с разрешения редакции.
Работает на WMS 2.34 (Страница создана за 0.11 секунд (Общее время SQL: 0.08 секунд - SQL запросов: 53 - Среднее время SQL: 0.00151 секунд))
Top.Mail.Ru