html5labs

Gastposting Dariusz Parys, Developer Evangelist für .NET 3.0 Technologie, C++, High Performance Computing, Windows Communication Foundation, Workflow Foundation und Visual Studio Team System bei der Microsoft Deutschland GmbH.

Mit den HTML5 Labs stellen wir für interessierte Entwickler Prototyp-Implementierungen von noch nicht ausgereiften W3C HTML5 Spezifikationen bereit. Ich habe mir mal die IndexedDB Implementierung angeschaut und dabei die Frage aufgegriffen, wie man die JavaScript-Funktionalität des Internet Explorers erweitern kann. Die Antwort lautet Component Object Model oder kurz COM.

Installation

Die Implementierung kommt mit zwei COM-Objekten daher, welche unter der Haube den Microsoft SQL-Server Compact 4 mit einer synchronen und mit einer asynchronen Schnittstelle benutzen. Die Installation des Prototypen ist sehr einfach. Zuerst lädt man sich das Paket herunter. Danach registriert man in einer Eingabeaufforderung als Administrator die entsprechende COM-DLL mittels regsvr32:

regsvr32 sqlcejse40.dll

Nach der Registrierung muss man den IE9 allerdings auch priviligiert starten, um mit dem Prototyp zu arbeiten.

Zum Start benötigt man immer ein Initialisierungsskript. Schließlich ist die Funktionalität in der COM-Komponente enthalten und wir möchten das ganze aus JavaScript heraus nutzen. Das Paket kommt mit einem solchen Skript und nennt sich bootidb.js. Ein Blick in die entsprechende Codezeile verrät uns, dass auf dem window-Element entsprechend eine Eigenschaft indexedDB hinzugefügt wird:

  1: if (!window.indexedDB) {
  2:     window.indexedDB = new ActiveXObject("SQLCE.Factory.4.0");
  3:     window.indexedDBSync = new ActiveXObject("SQLCE.FactorySync.4.0");

Die Möglichkeit, jedes IDispatch-fähige Objekt in das HTML-DOM des Browser zu stecken, war mir so nie bewusst. Schaut man sich einmal die Typelibrary der COM-Objekte an, so sieht man die Methoden, welche man schließlich aus JavaScript heraus direkt benutzen kann:

  1: [ 
  2:   odl, 
  3:   uuid(567270C8-E61A-4A8D-9C2E-2D2EC47D8704), 
  4:   helpstring("ObjectStore - Asynchronous interface"), 
  5:   dual, 
  6:   nonextensible, 
  7:   oleautomation 
  8: ] 
  9: interface IDBObjectStoreRequest : IDBObjectStore {
 10:     [id(0x60040000)]
 11:     HRESULT put(
 12:                     [in] VARIANT value, 
 13:                     [in, optional] VARIANT key, 
 14:                     [out, retval] IDBRequest** ppWebRequest);
 15:     [id(0x60040001)]
 16:     HRESULT add(
 17:                     [in] VARIANT value, 
 18:                     [in, optional] VARIANT key, 
 19:                     [out, retval] IDBRequest** ppWebRequest);
 20:     [id(0x60040002)]
 21:     HRESULT get(
 22:                     [in] VARIANT key, 
 23:                     [out, retval] IDBRequest** ppWebRequest);
 24:     [id(0x60040003)]
 25:     HRESULT remove(
 26:                     [in] VARIANT key, 
 27:                     [out, retval] IDBRequest** ppWebRequest);
 28:     [id(0x60040004)]
 29:     HRESULT createIndex(
 30:                     [in] BSTR name, 
 31:                     [in] BSTR keyPath, 
 32:                     [in, optional] VARIANT unique, 
 33:                     [out, retval] IDBRequest** ppWebRequest);
 34:     [id(0x60040005)]
 35:     HRESULT index(
 36:                     [in] BSTR indexName, 
 37:                     [out, retval] IDBRequest** ppWebRequest);
 38:     [id(0x60040006)]
 39:     HRESULT deleteIndex(
 40:                     [in] BSTR indexName, 
 41:                     [out, retval] IDBRequest** ppWebRequest);
 42:     [id(0x60040007)]
 43:     HRESULT openCursor(
 44:                     [in, optional] VARIANT range, 
 45:                     [in, optional] VARIANT direction, 
 46:                     [out, retval] IDBRequest** ppWebRequest);
 47: };
 48: 

Wichtig: Diese Registrierung ist nur notwendig für die Prototyp-Implementierung. Wenn die IndexedDB-Spezifikation einen stabilen Zustand erreicht und die Funktionalität in den Browser aufgenommen wird, so wird die Implementierung Bestandteil des Browsers und kann somit auch direkt aus JavaScript heraus genutzt werden.

IndexedDB in Action

Nach der Installation können wir die Funktionalität direkt nutzen. Im Paket sind neben den Readme’s und der DLL auch Beispiele enthalten. Eines der Beispiele ist ein BugTracking-System. Lädt man die Seite nun lokal in den Browser, so kann man direkt Bugs erfassen und danach auch wieder suchen:

bugtracking with indexeddb

Das untere Bild zeigt, wie man nach dem “Test Projekt” sucht und die Ergebnisse angezeigt bekommt:

searching in indexeddb

Die Daten werden letztlich in einer SQL CE Datebank als serialisiertes JSON (JavaScript Object Notation) abgelegt. Mittels eines SQL-Server Management Studios kann man auch einen Blick in die Datenbank werfen. Diese findet man unter %localappdata%\Microsoft\IndexedDatabase. Man öffnet diese einfach über den Dialog des Management Studios und schon hat man Zugriff auf die Datenbank:

indexeddb in sql management studio

Aufgrund der Binärfelder, die im Schema benutzt werden, kann man allerdings ohne Konvertierungen die Werte nicht direkt einsehen:

indexeddb quried in sql management studio

Unter der Haube

Die Funktionalität der BugTracker-Beispielanwendung ist schnell ausprobiert, doch jetzt möchte ich selbst mal eine Datenbank anlegen und Daten speichern. Zuerst einmal entscheide ich mich für die asynchrone Variante der API, da jeder synchrone Aufruf die Browseranfrage blocken kann. Das möchte man in der Regel nicht, da die Webseite sonst - wie sagt man so schön - nicht “responsive” ist. Eine einfache TODO Liste soll hier als Beispiel dienen. Zuerst definiere ich das UI:

  1: <!DOCTYPE html>
  2: 
  3: <html lang="en">
  4:     <head>
  5:         <meta charset="utf-8" />
  6:         <title></title>
  7:     </head>
  8:     <body>
  9:         <input type=text id=todoitem />
 10:         <input type=submit value=add id=actionAdd />
 11:         <div id=todos></div>
 12:     </body>
 13: </html>

Um IndexedDB zu benutzen, brauche ich das Init-Script. Zudem setze ich einen Handler auf den Submit-Button, der aufgerufen wird, sobald dieser gedrückt wird:

  1: <script type="text/javascript" src="bootidb.js"></script> 
  2: <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.4.min.js"></script> 
  3: <script type="text/javascript"> 
  4:     $(document).ready( function() 
  5:     { 
  6:         $("#actionAdd").click(function() { 
  7:         }); 
  8:     }); 
  9: </script>

Der Rahmen ist gelegt; nun geht es um die Daten. In dem mitgeliefertem Beispiel wird ein JavaScript-Objekt erzeugt, welches Variablen und Funktionen vorhält. Ich habe mich an diese Vorgehensweise gehalten, da es bei asynchronen Aufrufen durchaus sinnvoll ist, alles in einander wiederzuverwenden.

Die eigentliche Implementierung bedient sich auch ein paar Hilfsfunktionen, aber im Großen und Ganzen läuft es auf diesen Code zum Erstellen der Datenbank hinaus:

  1: requestDatabase = window.indexedDB.open(todoDB.db, 'Todo Database');

Das Öffnen der Datenbank legt auch gleichzeitig diese an, falls sie noch nicht vorhanden ist. Nun muss man prüfen, ob die Tabelle, mit der man arbeiten möchte, schon erzeugt ist. Die Prüfung ist im Gesamtcode zum Blogpost enthalten. Ich möchte hier nur noch aufzeigen, wie man letztlich die Tabelle anlegt und gleich noch einen Index hinzufügt:

  1: requestObjectStore = database.createObjectStore( 
  2:                                 todoDB.objectStore, 
  3:                                 todoDB.keyPath, 
  4:                                 true ); 
  5: requestObjectStore.onsuccess = function(evt) 
  6: { 
  7:     store = evt.result; 
  8:     requestIndex = store.createIndex( 
  9:                             todoDB.indexTodo,
 10:                             todoDB.indexTodoKeyPath,
 11:                             false);
 12:     requestIndex.onsuccess = function(evt)
 13:     {
 14:         whenReady();
 15:     }
 16: }

Zur Vereinfachung habe ich die onerror-Methoden entfernt. Zeile 1 legt die Tabelle an, Zeile 8 den entsprechenden Index auf ein Tabellenfeld. Benutzt man einen Index, hat man die Möglichkeit, mit dem Cursor gezielt zu arbeiten und bei einer Suche auch nur Teilbereiche zu ermitteln. Anders ist es, wenn man keinen Index hat. Da macht man einfach einen Tablescan, bis man den gesuchten Datensatz hat.

Auf der anderen Seite bietet die IndexedDB auch die Möglichkeit, immer über den Primärschlüssel direkt auf einen Datensatz zuzugreifen. Zum Beispiel ein Datensatz mit dem Primary Key ID 723 kann mittels

store.get(723)

geholt werden. ISAM Datenbanken lassen grüßen. Nun der vollständige Code einer einfachen HTML5 TODO List:

  1: <!DOCTYPE html>
  2: 
  3: <html lang="en">
  4:     <head>
  5:         <meta charset="utf-8" />
  6:         <title></title>
  7:         <meta http-equiv="X-UA-Compatible" content="IE=8" />
  8:         <script type="text/javascript" src="bootidb.js"></script>
  9:         <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.4.min.js"></script>
 10:         <script type="text/javascript">
 11: 
 12:             function commitTransaction(txn)
 13:             {
 14:                 try
 15:                 {
 16:                     if (txn)
 17:                     {
 18:                         txn.oncomplete = function ()
 19:                         {
 20:                             if (txn.db)
 21:                                 txn.db.close();
 22:                         }
 23:                         txn.commit();
 24:                     }
 25:                 }
 26:                 catch (e)
 27:                 {
 28:                     output_trace("Error in commitTransaction(): " + e.message);
 29:                 }
 30:             }
 31:             
 32:             function abortTransaction(txn)
 33:             {
 34:                 try
 35:                 {
 36:                     if (txn)
 37:                     {
 38:                         txn.onabort = function ()
 39:                         {
 40:                             if (txn.db)
 41:                                 txn.db.close();
 42:                         }
 43:                         txn.abort();
 44:                         alert('transaction aborted');
 45:                     }
 46:                 }
 47:                 catch (e)
 48:                 {
 49:                     output_trace("Error in abortTransaction(): " + e.message);
 50:                 }
 51:             }        
 52:             
 53:             function output_trace(message)
 54:             {
 55:                 var trace = $("#trace");
 56:                 if ((trace != null ) || (trace != undefined))
 57:                     trace.html(message);
 58:             }
 59:             
 60:             Array.prototype.contains = function(obj) {
 61:                 try
 62:                 {
 63:                 var i = this.length;
 64:                 while (i--) {
 65:                     if (this[i] === obj) {
 66:                     return true;
 67:                     }
 68:                 }
 69:                 return false;
 70:                 }
 71:                 catch (e)
 72:                 {
 73:                     output_error("Error in Array.prototype.contains(): " + e.message);
 74:                 }
 75:             };
 76: 
 77: 
 78:             var todoDB = {};
 79:             
 80:             todoDB.db = "todolist";
 81:             todoDB.objectStore = "todos";
 82:             todoDB.keyPath = "id";
 83:             todoDB.indexTodo = "todo";
 84:             todoDB.indexTodoKeyPath = "todo";
 85:             todoDB.searchResults = [];
 86: 
 87:             todoDB.addTodo = function( todoitem )
 88:             {
 89:                 var database = null;
 90:                 try
 91:                 {
 92:                     var requestDatabase = null;
 93:                     var requestObjectStore = null;
 94:                     var requestPut = null;
 95:                     var requestIndex = null;
 96:                     var store = null;
 97:                     var transaction = null;
 98:                     
 99:                     requestDatabase = window.indexedDB.open(todoDB.db, 'Todo Database');
100:                     requestDatabase.onsuccess = function( evt )
101:                     {
102:                         database = evt.result;
103:                         var whenReady = function()
104:                         {
105:                             transaction = database.transaction();
106:                             var objectStore = transaction.objectStore(todoDB.objectStore);
107:                             var newEntry = { todo: todoitem };
108:                             requestPut = objectStore.put(newEntry);
109:                             
110:                             requestPut.onsuccess = function(evt)
111:                             {
112:                                 commitTransaction(transaction);
113:               }
114:                             requestPut.onerror = function(evt)
115:                             {
116:                                 output_trace("requestPut.onerror = " + evt.message);
117:                                 abortTransaction(transaction);
118:               }
119:             };
120:                         
121:                         if( database.objectStoreNames.contains(todoDB.objectStore) )
122:                         {
123:                             whenReady();
124:             }
125:                         else
126:                         {
127:                             requestObjectStore = database.createObjectStore(
128:                                                             todoDB.objectStore,
129:                                                             todoDB.keyPath,
130:                                                             true );
131:                             requestObjectStore.onsuccess = function(evt)
132:                             {
133:                                 store = evt.result;
134:                                 requestIndex = store.createIndex(
135:                                                         todoDB.indexTodo,
136:                                                         todoDB.indexTodoKeyPath,
137:                                                         false);
138:                                 requestIndex.onsuccess = function(evt)
139:                                 {
140:                                     whenReady();
141:                 }
142:                                 requestIndex.onerror = function(evt)
143:                                 {
144:                                     output_trace("requestIndex.onerror = " + evt.message);
145:                                     abortTransaction(transaction);
146:                 }
147:               }
148:                             requestObjectStore.onerror = function(evt)
149:                             {
150:                                 output_trace("requestObjectStore.onerror = " + evt.message);
151:                                 abortTransaction(transaction);
152:               }
153:             }
154:           }
155:                     requestDatabase.onerror = function(evt)
156:                     {
157:                         output_trace("requestDatabase.onerror = " + evt.message);
158:                         abortTransaction(transaction);
159:           }
160:         }
161:                 catch (e)
162:                 {
163:                     output_trace("catch(e) = " + e.message);
164:                     if(database)
165:                     {
166:                         database.close();
167:           }
168:         }
169:       }
170:             
171:             todoDB.findTodos = function () { 
172: 
173:                 var database = null;
174:                 try
175:                 {
176:                     var requestDatabase = null;
177:                     var requestObjectStore = null;
178:                     var requestIndex = null;
179:                     var requestCursor = null;
180:                     var requestMove = null;
181:                     var store = null;
182:                     var cursor = null;
183:                     var transaction = null;
184:                     var index = 1;
185:                     
186:                     requestDatabase = window.indexedDB.open(todoDB.db, 'Todo Database');    
187:                     requestDatabase.onsuccess = function (evt) 
188:                     {
189:                         database = evt.result;
190:                         transaction = database.transaction();
191:             
192:                         if (database.objectStoreNames.contains(todoDB.objectStore)) 
193:                         {
194:                             store = transaction.objectStore(todoDB.objectStore);
195:                             requestCursor = store.openCursor();
196:                             requestCursor.onsuccess = function(evt)
197:                             {
198:                                 var addRecord = function()
199:                                 {
200:                                     requestIndex = store.get(index);
201:                                     requestIndex.onsuccess = function(evt)
202:                                     {
203:                                         var record = evt.result;
204:                                         todoDB.searchResults.push(record.todo);
205:                                         index++;
206:                                         addRecord();
207:                   }
208:                                     requestIndex.onerror = function(evt)
209:                                     {
210:                                         commitTransaction(transaction);
211:                                         displayTodos();
212:                   }
213:                 }
214:                                 
215:                                 var cursor = evt.result;
216:                                 addRecord();
217:               }
218:                             requestCursor.onerror = function(evt)
219:                             {
220:                                 trace_output("requestCursor.onerror = " + evt.message);
221:                                 abortTransaction(transaction);
222:               }
223:                         }
224:                     }
225:                 }
226:                 catch (e)
227:                 {
228:                     output_trace("Error in bugDB.findProjectSearchResults(): " + e.message);
229:                     if (db)
230:                             db.close();    
231:                     
232:                 }
233:             };
234:             
235:             function displayTodos()
236:             {
237:                 var result = "<ul>";
238:                 $.each(todoDB.searchResults, function()
239:                 {
240:                     result += "<li>" + this + "</li>";
241:         });
242:                 result += "</ul>";
243:                 $("#todos").html(result);
244:       }
245:         
246:             $(document).ready( function()
247:             {
248:                 $("#actionAdd").click(function() {
249:                     var todoItem = $("#todoitem").val();
250:                     todoDB.addTodo(todoItem);
251:         });
252:                
253:                 $("#actionFind").click(function() {
254:                     todoDB.searchResults = [];
255:                     todoDB.findTodos();     
256:         });
257:                
258:       });
259:     </script>
260:     </head>
261:     <body>
262:         <input type=text id="todoitem" />
263:         <input type=submit value=add id="actionAdd" />
264:         <input type=submit value=refresh id="actionFind" />
265:         <h2>Todos:</h2>
266:         <div id="todos"></div>
267:         <div id="trace"></div>
268:     </body>
269: </html>

Das UI ist nicht sonderlich hübsch, aber es geht ja erst einmal nur um die Funktionalität:

todo-list-in-action

Fazit

Eine indexbasierte Datenbank im Browser des Clients zu haben, kann für manche Anwendungsszenarien sinnvoll sein. So können zukünftige HTML 5-Anwendungen offline betrieben werden (ja, auch dafür gibt es Specs), die zum Beispiel auch Referenzdaten benötigen. Die Verwendung der API ist Stand heute noch ein wenig mühselig, auch direktes Tooling im Browser fehlt noch komplett. Trotzdem ist es ein erster funktionaler Ansatz, der auf Feedback wartet.