In meinem letzten Artikel bin ich auf die Vorzüge von TypeScript
eingegangen und habe gesagt dass ich es jetzt überall einsetze. Also habe ich mir gedacht, wieso nicht mal eine Anwendung
machen die auf einer gemeinsamen Codebasis im Web und sogar auf Windows 8 läuft!
Ich habe also angefangen meine Anwendung zuerst für Webclients zu bauen, so dass man mit dem Desktop die Anwendung per
Browser nutzen kann. Da ich ein großer Fan von knockout bin, habe ich es natürlich auch
in dieser Anwendung verwendet. Außerdem verwende ich TypeScript um meine Objekte
und Funktionen typsicher zu machen.
Die Anwendung soll dazu dienen persönliche Bilanzdaten anzuzeigen, die über eine REST Schnittstelle per ASP.NET Web API zur Verfügung gestellt werden. Die Implementierung der jeweiligen
Controller ist nicht Bestandteil dieses Beitrags.
Das Datenaustauschformat ist in diesem Fall JSON. Mit ASP.NET Web API ist es auch
sehr einfach möglich die Daten im XML-Format anzufordern. Dazu aber vielleicht in einem anderen Beitrag mehr 😉
Los geht’s:
Zuerst machen wir uns ein AppViewModel:
1: ///<reference path='../../declaretypes/knockout.d.ts'/>
2: ///<reference path='../../declaretypes/jquery.d.ts'/>
3: ///<reference path='../../declaretypes/app.d.ts'/>
4: ///<reference path='Data.ts'/>
5: class AppViewModel {
6: selectedBilanz: koObservable;
7: bilanzen: koObservableArray; // Alle Bilanzen (aktuelle, zum Vorjahr usw.)
8: ... noch mehr ...
9: constructor () {
10: this.bilanzen = ko.observableArray([]);
11: this.selectedBilanz = ko.observable(null);
12: ... noch mehr ...
13: this.loadBilanzen();
14: }
15: loadBilanzen () {
16: var that = this;
17: Data.getBilanzOverview(function (data) {
18: that.bilanzen(data);
19: });
20: }
21: addNewBilanz() {
22: var newitem = { Name: "Neu", Beschreibung: "Neue Bilanz" };
23: this.addEmptyBilanz(newitem);
24: }
25: addEmptyBilanz(emptynew:
IEmptyBilanz) {
26: var that = this;
27: Data.AddBilanzOverview(emptynew, function (data) {
28: Data.createPropertys
(data);
29: that.bilanzen.push
(data);
30: });
31: }
32: deleteBilanz() {
33: var that = this;
34: Data.RemoveBilanzOverview (this.selectedBilanz().ID(), function (data) {
35:
36: that.bilanzen.remove
(that.selectedBilanz());
37: that.selectedBilanz(null);
38: });
39: }
40: ... noch mehr ...
41: }
Wie man sieht, hat die App ein paar Objekte die überwacht werden sollen und vom Typ koObservable und koObservableArray
sind. Im Konstruktor werden alle Properties initialisiert und die Funktion zum Laden der Bilanzdaten aufgerufen. Das
laden/speichern der Daten wurde hier in das Modul Data ausgelagert.
1: module Data {
2: var OverviewUrl = "http://servername/api/Overview/";
3:
4: ... weitere urls ...
5:
6: /*
7: Returns BilanzOverview from current User -> Hardcoded in Controller.
8: */
9: export function getBilanzOverview(callback: (data:any) => any) {
10:
11: $.getJSON(OverviewUrl, function(data) {
12: //Daten aufbereiten und dann callback aufrufen
13: callback(data);
14: });
15: }
16: ... weitere Funktionen
17: }
Die Typen werden in einem declare File deklariert und dann in der jeweiligen Datei referenziert. Beispiel für
knockout.d.ts:
1: declare module ko {
2: export function observable(obj?): any;
3: export function observableArray(obj?): any;
4: export function toJSON(koObj): any;
5: export module utils {
6: export function compareArrays(oldArray, newArray): any;
7: }
8: }
9: interface koObservable {
10: (obj?): any;
11: }
12: interface koObservableArray {
13: (obj?): any;
14: push: (obj) => any;
15: remove: (obj) => any;
16: }
In dieser Datei befinden sich alle Definitionen, die ich von knockout für diese
Applikation verwende. Wenn noch andere Funktionalitäten aus knockout benötigt werden,
kann man diese im declare File nachtragen um die Verwendung von knockout a) typsicher zu
machen und b) IntelliSense Unterstützung in Visual Studio 2012 zu bekommen.
Mit dieser minimalen Anwendung kann man nun schon die ersten Daten anzeigen lassen und hinzufügen und entfernen. Hierfür
gibt es folgendes knockout Template.
1: <div class="bilanz-buttons">
2: <button data-bind="click: addNewItem">Add</button>
3: <br />
4: <button data-bind="click: deleteItem">Delete</button>
5: </div>
6: <div class="bilanzlist">
7: <div class=" bilanz-tmpl" data-bind="foreach: bilanzen">
8: <div class="bilanz tile-color" data-bind="css:{itemSelected: $root.isSelectedBilanz($data) },
9: click: function(){ $parent.setSelectedBilanz($data) }">
10: <h3 data-bind="text: Name "></h3>
11: <p data-bind="text: Beschreibung"></p>
12: </div>
13: </div>
14: </div>
Lässt man die Anwendung nun laufen werden im Browser die persönlichen Bilanzen angezeigt. Beim auswählen einer Bilanz wird
die entsprechende Bilanz in die selectedBilanz Property geschrieben um dazu später die entsprechenden Kategorien zu laden.
Beim Drücken von Add und Delete werden neue Bilanzen erzeugt bzw. gelöscht. Bisher kein großes Hexenwerk 🙂
Include Windows 8
Was kann ich von dem bisher geschriebenen alles für meine Windows 8 App wiederverwenden? Nun Ja, ich würde sagen eine
ganze Menge 🙂
Zunächst einmal kann das komplette AppViewModel und auch das komplette Data Modul verwendet werden.
Cool oder?
Wir erzeugen jetzt ein W8AppViewModel und leiten von dem AppViewModel ab, zusätzlich legen wir noch eine
WinJS.Binding.List an um diese dann an ein ListView zu binden:
1: var bilanzList = new WinJS.Binding.List([]);
2: WinJS.Namespace.define("Bilanzen", { itemList: bilanzList });
3:
4: class W8AppViewModel extends AppViewModel {
5: lastArray;
6: iteminvoke;
7: constructor() {
8: super();
9: var that = this;
10: this.lastArray = [];
11: this.bilanzen.subscribe(function (newValue) {
12: Utils.UpdateArray
(that.lastArray, newValue, bilanzList);
13: that.lastArray =
newValue.slice();
14: });
15:
16: // Handler for iteminvoke.
17: this.iteminvoke = WinJS.Utilities.markSupportedForProcessing(function (evt) {
18: that.itemselected(evt,
that);
19: });
20: });
21: }
22: itemselected(evtInfo,vm:
W8AppViewModel) {
23: var index = evtInfo.detail.itemIndex;
24: vm.setSelectedBilanz(vm.bilanzen
()[index]);
25: }
26: ... weitere Funktionen ...
27: }
Es wird der Konstruktor aufgerufen, der wiederrum den Konstruktor der AppViewModel aufruft. Danach „abonnieren“
wir das Observable Array per this.bilanzen.subscribe(callback) um informiert zu werden, wenn sich irgendwas in dem
Array ändert. Ändert sich etwas, aktualisieren wir die DataSource des ListView im Callback der subscribe Funktion. Zusätzlich
wollen wir noch informiert werden wenn in der ListView eine Bilanz ausgewählt wird um später die entsprechenden Kategorien zu
laden. Hierfür bietet das ViewModel einen iteminvoke-handler an. Dieser wird dann in der View an das entsprechende
oniteminvoke-Event der ListView gebunden. Das hinzufügen und entfernen von Bilanzen befindet sich im
AppViewModel und kann genau so genutzt werden. Mit diesem ViewModel können wir uns nun um
das Anzeigen der Daten kümmern.
Wie schon erwähnt nutzen wir dafür ein ListView:
1: <div class="bilanz-buttons">
2: <button data-bind="click: addNewItem">Add</button>
3: <br />
4: <button data-bind="click: deleteItem">Delete</button>
5: </div>
6: <div class="" id="bilanzen- tmpl" data-win-control="WinJS.Binding.Template">
7: <div class="bilanz tile-color">
8: <h3 data-win-bind="innerText: w8_Name"></h3>
9: <p data-win-bind="innerText: w8_Beschreibung"></p>
10: </div>
11: </div>
12: <div class="bilanzlist" id="bilanzlistview" data-win-control="WinJS.UI.ListView"
13: data-win-options="{
14: itemDataSource :
Bilanzen.itemList.dataSource,
15: tapBehavior:
'toggleSelect',
16: selectionMode: 'single',
17: oniteminvoked:
ViewModel.iteminvoke,
18: itemTemplate: select
('#bilanzen-tmpl')}">
19: </div>
Der aufmerksame Leser wird festgestellt haben, dass hier auf andere Properties gebunden wird als in dem HTML weiter oben.
Das liegt daran das WinRT nicht an Funktionen binden kann. Da wir hier aber knockout
verwenden und alle Properties über ko.observable() und ko.observableArray() deklariert haben, sind unsere
Properties also Funktionen und somit nicht bindable :/ Blöd!
Als Workaround dafür habe ich in dem Data Modul eine Funktion geschrieben die mir für jedes Property noch ein
w8_Property erzeugt (und falls da Arrays und andere Objekte drin sind, werden die auch nochmal durchlaufen):
1: export function createPropertys(obj: any) {
2: for (var key in obj) {
3: if (typeof obj[key] !== "function") {
4: if (typeof obj[key] === "object") {
5: if (obj[key] instanceof Array) {
6: obj[key] =
ko.observableArray(obj[key]);
7: for (var i = 0; i < obj[key]().length; i++) {
8:
createPropertys(obj[key]()[i]);
9: }
10: } else {
11: createPropertys(obj
[key]);
12: }
13: } else {
14: obj[key] =
ko.observable(obj[key]);
15: if (window["WinJS"]) {
16: Object.defineProperty(obj, "w8_" + key, {
17: get:
ko.observable(obj[key])(),
18: enumerable: true
19: });
20: }
21: }
22: }
23: }
24: }
Damit hat man dann die Möglichkeit in dem Datatemplate auf diese Properties zu binden.
Den Handler für das oniteminvoke-Event kann man ebenfalls über einen Namespace zur Verfügung stellen.
Gemacht habe ich das ganze in dem onactivate-Handler der Applikation.
1: app.onactivated = function (args) {
2: if (args.detail.kind === activation.ActivationKind.launch) {
3: if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
4: // TODO: This application has been newly launched. Initialize
5: // your application here.
6: } else {
7: // TODO: This application has been reactivated from suspension.
8: // Restore application state here.
9: }
10:
11: var appvm = new W8AppViewModel();
12: WinJS.Namespace.define("ViewModel", appvm);
13:
14: args.setPromise (WinJS.UI.processAll().then(function () {
15: ko.applyBindings
(appvm);
16: }));
17:
18: }
19: };
So, nun sind wir an dem Punkt angelangt wo wir unsere Windows 8 Store App laufen lassen können. Diese baut jetzt auf dem
schon vorhanden Code der Webapplikation auf, erweitert diesen mit einigen kleinen Funktionen und läuft nun wunderbar als
Windows Store App.
Ich habe hier nicht die komplette Anwendung erklärt, aber in den folgenden Screenshots könnt ihr euch mein bisheriges
Ergebnis anschauen. Links Webbrowser(div,ul,li Elemente) und Rechts Metro (ListView, SemanticZoom, AppBarCommands).
Im Übrigen habe ich einfach die css-Datei von der Webapplikation genommen, in der Windows 8 Store App eingefügt und die
entsprechenden css-Klassen gesetzt. Einmal gemacht, mehrfach eingesetzt.
Fazit
Dieses kleine Beispiel soll aufzeigen, dass schon mit geringem Aufwand der Code von einer vorhanden Webapplikation genutzt
werden kann um damit eine Windows Store 8 App zu erstellen. Wichtig dabei war die Verwendung von Windows 8 Controls ohne
dabei den Code für die Webapplikation zu verändern. Es ist ein wenig schade, dass das Databinding in WinJS nicht so
komfortabel ist wie z.B. mit knockout aber ich denke, wenn man sich damit arrangieren
kann bekommt man gute Lösungen hin. Vielleicht gibt es ja in der Zukunft Bibliotheken die sich diesem Thema annehmen.
Zum Schluss bleibt mir nur zu sagen, dass ich positiv überrascht war, das man soviel geschriebenen Code einfach 1 zu 1 in
einer Windows Store App übernehmen kann. Allerdings sollte man sich auch klar machen, dass unterschiedliche Plattformen auch
immer unterschiedliche Möglichkeiten und Bedienkonzepte bieten. Auf diese muss man beim Design der Anwendung natürlich
Rücksicht nehmen. Bei bestehenden Anwendungen kann das natürlich durchaus problematisch sein. Setzt man sich aber mit den
plattformspezifischen Features auseinander und geht an der einen oder anderen Stelle einen Kompromiss ein, kommt man dem
One Application, Multiple Devices schon sehr nahe. Daher gibt es für TypeScript + knockout + WinRT ein klares Yeah!