In genere, nello sviluppo di un'applicazione Windows Store in HTML5/JavaScript i controlli HTML (come button
e page
) e quelli inclusi nella libreria WinJS (come FlipView
, ListView
, DatePicker
, ecc.) sono più che sufficienti a coprire la gran parte degli scenari (senza contare i controlli di WinRT accessibili da JavaScript tramite i meccanismi di language projection, come ad esempio Windows.UI.Popups.PopupMenu
). In alcuni casi, tuttavia, può essere utile definire propri controlli custom per sfruttare nuove e diverse funzionalità non fornite dai controlli "di serie". Inoltre, un controllo custom può essere riutilizzato in più applicazioni centralizzando la scrittura, il debug e l'aggiornamento del controllo in un unico progetto.
Per raggiungere questo obiettivo, è utile osservare come funzionano i controlli WinJS. Questa libreria combina infatti HTML, stili CSS e, soprattutto, il codice JavaScript per incapsulare la logica del controllo. È possibile osservare il codice di tutti i controlli esposti da WinJS semplicemente esplorando il file ui.js
, presente in tutte le applicazioni Windows Store in HTML5/JavaScript.
Questa libreria può essere sfruttata anche per creare propri componenti custom, o per estendere controlli esistenti, semplicemente utilizzando gli stessi pattern usati per i controlli standard. Rispetto all'alternativa di dover scrivere i propri controlli partendo da zero, WinJS si fa carico di molti dei dettagli implementativi, lasciando lo sviluppatore libero di concentrarsi sulla logica del controllo, piuttosto che sull'ottimizzazione del codice o la gestione della memoria.
La prima cosa da osservare è come il codice dell'intero controllo sia incluso all'interno di una cosiddetta "self-executing anonymous function" eseguita all'avvio dell'applicazione (da notare come anche il codice contenuto nel file default.js
sia anch'esso incapsulato in una funzione di questo tipo). In una anonymous function, tutte le variabili e le funzioni definite al suo interno non sono accessibili dall'esterno e sono quindi isolate rispetto al resto del codice, riducendo al minimo la possibilità di conflitti o involontarie modifiche di una variabile. Una funzione self-executing ha la seguente struttura:
(function mySelfExecutingFunction(){
// ...codice da eseguire ...
})();
In secondo luogo, è da notare come tutti i controlli esposti dalla libreria siano implementati nel namespace WinJS.UI
. Ad esempio, il seguente codice, estrapolato a titolo d'esempio direttamente dal file ui.js
, definisce il namespace per il controllo DatePicker
:
WinJS.Namespace.define("WinJS.UI", { ... }
Il controllo vero e proprio è creato tramite la funzione WinJS.Class.define(), che permette di definire la classe che incapsula la logica del controllo e sfrutta una serie di funzioni helper per la creazione di proprietà ed eventi:
var DatePicker = WinJS.Class.define(...);
Questa funzione accetta tre parametri, ciascuno dei quali definisce un particolare aspetto della classe:
- Il costruttore da utilizzare per istanziare il controllo;
- un oggetto che contiene i campi, le proprietà e i metodi di istanza disponibili per quel tipo;
- infine, un oggetto che definisce i campi, le proprietà e i metodi statici del controllo stesso.
Vediamoli più in dettaglio con alcuni esempi.
Quanto al primo parametro, ossia il costruttore della classe che incapsula la logica interna del controllo, è interessante osservare come tutti i controlli della libreria definiscano un costruttore che accetta gli stessi due parametri, element
e options
. Il primo punta all'elemento DOM che conterrà il controllo. Se viene passato un valore null
, verrà assegnato un valore di default; al tempo stesso, l'elemento mantiene una reference al controllo tramite la proprietà winControl
.
Il secondo parametro invece, può essere usato per impostare eventuali opzioni relative al controllo. Il prossimo snippet ne mostra un esempio:
var DatePicker = WinJS.Class.define(function DatePicker_ctor(element, options) {
//...
element = element || document.createElement("div");
element.winControl = this;
// ...
WinJS.UI.setOptions(this, options);
// ...
La funzione helper WinJS.UI.setOptions provvederà poi a iterare tra le varie opzioni e ad assegnare i relativi valori ai campi con lo stesso nome presenti nel controllo target (è anche possibile passare handler per gli eventi esposti dal controllo, facendo convenzionalmente precedere il nome della funzione dal prefisso "on"; in questo caso, la funzione helper provvederà a invocare il metodo addEventListener
per associare l'handler all'evento corrispondente).
Il prossimo snippet mostra la definizione della relativa funzione, particolarmente utile anche nella creazione di controlli custom:
function _setOptions(control, options, eventsOnly) {
if (typeof options === "object") {
var keys = Object.keys(options);
for (var i = 0, len = keys.length; i < len; i++) {
var key = keys[i];
var value = options[key];
if (key.length > 2) {
var ch1 = key[0];
var ch2 = key[1];
if ((ch1 === 'o' || ch1 === 'O') && (ch2 === 'n' || ch2 === 'N')) {
if (typeof value === "function") {
if (control.addEventListener) {
control.addEventListener(key.substr(2), value);
continue;
}
}
}
}
if (!eventsOnly) {
control[key] = value;
}
}
}
};
Il secondo parametro da passare al metodo WinJS.Class.define
è rappresentato dalle proprietà e dai metodi di istanza per quel particolare controllo.
Il seguente snippet evidenzia una (piccola) parte delle proprietà e dei metodi associati alla classe DatePicker
:
var DatePicker =
WinJS.Class.define(function DatePicker_ctor(element, options) {… },
{
...
_currentDate: null,
_calendar: null,
_disabled: false,
...
},
...
/// <field type="String" locid="WinJS.UI.DatePicker.calendar" helpKeyword="WinJS.UI.DatePicker.calendar">Gets or sets the calendar to use.</field>
calendar: {
get: function () {
return this._calendar;
},
set: function (value) {
this._calendar = value;
this._setElement(this._domElement);
}
},
/// <field type="Date" locid="WinJS.UI.DatePicker.current" helpKeyword="WinJS.UI.DatePicker.current">Gets or sets the current date of the DatePicker.</field>
current: {
get: function () {
var d = this._currentDate;
var y = d.getFullYear();
return new Date(Math.max(Math.min(this.maxYear, y), this.minYear), d.getMonth(), d.getDate(), 12, 0, 0, 0);
},
set: function (value) {
var newDate;
if (typeof (value) === "string") {
newDate = new Date(Date.parse(value));
newDate.setHours(12, 0, 0, 0);
}
else {
newDate = value;
}
var oldDate = this._currentDate;
if (oldDate != newDate) {
this._currentDate = newDate;
this._updateDisplay();
}
}
},
/// <field type="Boolean" locid="WinJS.UI.DatePicker.disabled" helpKeyword="WinJS.UI.DatePicker.disabled">
/// Gets or sets a value that specifies whether the DatePicker is disabled. A value of true indicates that the DatePicker is disabled.
/// </field>
disabled: {
get: function () { return this._disabled; },
set: function (value) {
if (this._disabled !== value) {
this._disabled = value;
// all controls get populated at the same time, so any check is OK
//
if (this._yearControl) {
this._monthControl.setDisabled(value);
this._dateControl.setDisabled(value);
this._yearControl.setDisabled(value);
}
}
}
},
...
Il fatto che il nome di alcune delle proprietà e dei metodi associati al controllo sia preceduto da un underscore significa che, per convenzione, quei metodi e quelle proprietà devono intendersi come "privati" (in JavaScript il carattere privato di un metodo ha una valenza diversa da quella tipica dei linguaggi object-oriented come C#, C++ o VB: far precedere il nome di una funzione o di una proprietà dall'underscore non impedisce di chiamare quella funzione o accedere a quella proprietà, ma si limita semplicemente a segnalarne il carattere privato; di conseguenza Visual Studio non mostrerà quel metodo o quel campo nell'IntelliSense).
Vale anche la pena notare come i membri privati siano esposti tramite getter e setter che incapsulano le relative logiche di validazione.
Oltre a proprietà e funzioni, un controllo espone solitamente anche una serie di eventi. Anche in questo caso, WinJS contiene una serie di funzioni helper che consentono di agganciare eventi al controllo. Prendendo nuovamente come esempio il controllo DatePicker
, è interessante osservare come la funzione anonima che incapsula il controllo si concluda con queste due righe di codice:
WinJS.Class.mix(WinJS.UI.DatePicker, WinJS.Utilities.createEventProperties("change"));
WinJS.Class.mix(WinJS.UI.DatePicker, WinJS.UI.DOMEventMixin);
La funzione WinJS.Class.mix
permette di creare un cosiddetto "mixin", ossia un oggetto che definisce una o più funzionalità che possono essere aggiunte a un oggetto (il nostro controllo DatePicker
, in questo caso). La prima chiamata alla funzione WinJS.Class.mix
, in particolare, utilizza il metodo WinJS.Utilities.createEventProperties
per aggiungere un nuovo evento al controllo, che può essere quindi sottoscritto da codice, come mostrato nel seguente snippet (il prefisso "on" viene convenzionalmente aggiunto al nome dell'evento passato alla funzione createEventProperties
):
var picker = new WinJS.UI.DatePicker(element, options);
picker.onchange = function () {
// codice da eseguire al verificarsi dell'evento
};
La seconda chiamata, invece, consente al controllo di esporre metodi come addEventListener
, e removeEventListener
, che permettono di aggiungere o rimuovere event listener per gli eventi esposti dal controllo stesso, nonché il metodo dispatchEvent
, il quale viene invece usato per sollevare l'evento (quest'ultimo accettacome parametri il nome dell'evento e i dati ad esso associati). Questi metodi sono esposti dall'oggetto WinJS.UI.DOMEventMixin
, la cui definizione è la seguente:
WinJS.Namespace.define("WinJS.UI", {
DOMEventMixin: WinJS.Namespace._lazy(function () {
return {
_domElement: null,
addEventListener: function (type, listener, useCapture) {
(this.element || this._domElement).addEventListener(type, listener, useCapture || false);
},
dispatchEvent: function (type, eventProperties) {
var eventValue = document.createEvent("Event");
eventValue.initEvent(type, false, false);
eventValue.detail = eventProperties;
if (typeof eventProperties === "object") {
Object.keys(eventProperties).forEach(function (key) {
eventValue[key] = eventProperties[key];
});
}
return (this.element || this._domElement).dispatchEvent(eventValue);
},
removeEventListener: function (type, listener, useCapture) {
(this.element || this._domElement).removeEventListener(type, listener, useCapture || false);
}
};
}),
setOptions: setOptions,
_setOptions: _setOptions
});
})(this, WinJS);
Come si può intuire, WinJS.UI.DOMEventMixin
viene usato per creare e gestire eventi collegati a elementi DOM (come nel caso dei controlli, appunto, che generalmente incapsulano e manipolano elementi HMTL), mentre in caso di eventi gestiti unicamente via JavaScript, possiamo usare WinJS.UI.EventMixin
.
Creare un controllo custom
Come abbiamo già accennato, per creare un controllo custom possiamo sfruttare gli stessi pattern e le stesse funzioni helper usati da WinJS per definire i controlli standard. I passaggi necessari possono essere così riassunti:
- Incapsulare il controllo in una selft-executive anonymous function, in modo da isolare la logica implementativa dal resto del codice della tua app.
- Definire un namespace per il controllo custom tramite la funzione
WinJS.Namespace.define
. - Definire un costruttore per la classe che incapsula la logica del controllo e passare la funzione al metodo
WinJS.Class.define
come primo parametro.- Impostare eventuali opzioni di configurazione tramite il metodo helper
WinJS.UI.setOptions
. - Mantenere una reference all'elemento DOM che contiene il controllo e, contemporaneamente, salvare la reference al controllo nell'elemento stesso (tramite la proprietà
element.winControl
).
- Impostare eventuali opzioni di configurazione tramite il metodo helper
- Passare al metodo
WinJS.Class.define
, come secondo parametro, un oggetto che contiene i metodi e le proprietà da "attaccare" al controllo custom e, come terzo parametro un oggetto con i metodi e le proprietà statiche. - Creare e gestire gli eventi esposti dal controllo tramite la funzione
WinJS.Class.mix
.
Vediamo adesso un esempio di controllo custom.
(function helloWorldInit(global) {
"use strict";
// Constants definition
var DEFAULT_MESSAGE = "Ciao da Html.it",
CHANGE = "change";
var utilities = WinJS.Utilities;
// CSS class names
var helloWorld = "hello-world";
WinJS.Namespace.define("MyCustomLibrary.UI", {
HelloWorld: WinJS.Class.define(function HelloWorld_ctor(element, options) {
element = element || document.createElement("div");
element.winControl = this;
this._element = element;
options = options || {};
WinJS.UI.setOptions(this, options);
}, {
_message: DEFAULT_MESSAGE,
message: {
get: function () {
return this._message;
},
set: function (value) {
this._message = value;
this.element.innerText = value;
utilities.addClass(this.element, helloWorld);
this._onMessageChanged(value);
}
},
element: {
get: function () {
return this._element;
}
},
_onMessageChanged: function (newMessage) {
this.dispatchEvent("messagechanged", { message: newMessage });
}
})
});
WinJS.Class.mix(MyCustomLibrary.UI.HelloWorld,
WinJS.Utilities.createEventProperties("messagechanged"));
WinJS.Class.mix(MyCustomLibrary.UI.HelloWorld, WinJS.UI.DOMEventMixin);
})(this, WinJS);
Per prima cosa, il codice crea una funzione anonima che isola il codice del controllo dall'esterno, quindi definisce un namespace (MyCustomLibrary.UI
) e la classe che incapsula la logica del controllo (HelloWorld
). Questa classe espone due proprietà pubbliche: message
, che rappresenta il testo da visualizzare a schermo, ed element
, che rappresenta l'elemento DOM al cui interno verrà visualizzato il messaggio.
Ciascuna di queste proprietà fa riferimento (tramite i rispettivi getter e setter) a una corrispondente variabile "privata", contrassegnata dall'uso dell'underscore.
In particolare, quando la proprietà message
viene impostata, il codice all'interno del relativo setter provvederà ad aggiungere una classe CSS all'elemento DOM (helloWorld
) e a scatenare il corrispondente evento.
Terminata la funzione WinJS.Class.define
, le due chiamate al metodo WinJS.Class.mix
già visto in precedenza permettono, rispettivamente, di "attaccare" un evento al controllo (tramite la funzione WinJS.Utilities.createEventProperties
), e di sfruttare i metodi esposti dall'oggetto WinJS.UI.DomEventMixin
.
Il consumer del controllo può quindi sottoscrivere l'evento messagechanged
come mostrato nel seguente snippet.
var helloWorld = document.getElementById("helloWorld").winControl;
helloWorld.onmessagechanged = function (e) {
new Windows.UI.Popups.MessageDialog(e.message).showAsync();
}
Per testare il controllo custom usando la sintassi dichiarativa, possiamo usare la seguente definizione HTML come riferimento per la pagina di default. Quando la funzione WinJS.UI.processAll()
verrà invocata, il codice HTML verrà ispezionato e il controllo istanziato.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Demo.Html.it.MyCustomControl.JS</title>
<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
<!-- App4 references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
<script src="/js/myControl.js"></script>
</head>
<body>
<div id="helloWorld" data-win-control="MyCustomLibrary.UI.HelloWorld" data-win-options="{ message: 'Ciao da HTML.it'}"></div>
</body>
</html>
Estendere le funzionalità di un controllo già esistente
Piuttosto che creare un nuovo controllo da zero, a volte può essere utile estendere le funzionalità di un controllo già esistente. Una delle strade per raggiungere questo obiettivo consiste nel derivare il controllo esistente tramite la funzione WinJS.Class.derive. Il seguente snippet mostra l'implementazione di questo metodo:
function derive(baseClass, constructor, instanceMembers, staticMembers) {
if (baseClass) {
constructor = constructor || function () { };
var basePrototype = baseClass.prototype;
constructor.prototype = Object.create(basePrototype);
WinJS.Utilities.markSupportedForProcessing(constructor);
Object.defineProperty(constructor.prototype, "constructor", { value: constructor, writable: true, configurable: true, enumerable: true });
if (instanceMembers) {
initializeProperties(constructor.prototype, instanceMembers);
}
if (staticMembers) {
initializeProperties(constructor, staticMembers);
}
return constructor;
} else {
return define(constructor, instanceMembers, staticMembers);
}
}
Questo metodo implementa la cosiddetta ereditarietà prototipica (prototypical inheritance), salvando la classe base nella proprietà prototype della classe derivata. In questo modo, la nuova classe supporta tutte le funzionalità della classe base (oltre alle funzionalità proprie di quella derivata).
Il seguente codice mostra un esempio di utilizzo della funzione WinJS.Class.derive
per estendere il controllo standard rating di WinJS.
(function (global) {
"use strict";
WinJS.Namespace.define("MyLibrary", {
MyRating: WinJS.Class.derive(WinJS.UI.Rating,
function MyRating_ctor(element, options) {
WinJS.UI.Rating.apply(this, [element, options]);
},
{
_createControl: function () {
WinJS.UI.Rating.prototype._createControl.call(this);
var html = "<div>0</div>";
this._element.insertAdjacentHTML('beforeend', html);
this._ratingNumberElement = this._element.lastElementChild;
},
_updateControl: function () {
WinJS.UI.Rating.prototype._updateControl.call(this);
this._ratingNumberElement.innerText = this._userRating;
},
_showTentativeRating: function () {
WinJS.UI.Rating.prototype._showTentativeRating.call(this);
this._ratingNumberElement.innerText = this._tentativeRating;
}
})
});
})(this, WinJS);
La classe da derivare viene passata come primo parametro al metodo WinJS.Class.derive
. Gli altri parametri sono gli stessi usati per la funzione WinJS.Class.define
vista in precedenza, ossia il costruttore della funzione, un oggetto che contiene le proprietà e i metodi di istanza, nonché le funzioni e le proprietà statiche del controllo.
Per invocare il costruttore della classe base, è necessario passare la funzione WinJS.UI.Rating.apply. Per invocare le funzioni esposte dalla classe base, invece, occorre passare dalla proprietà prototype
, la quale, come si è già accennato, mantiene una reference alla classe originaria (WinJS.UI.Rating
, in questo caso).