Add Persistence to Your XML Data Islands

n last month’s article, you saw how to display XML data in IE5 through IE’s data island component. This month’s installment focuses on letting users modify that data and, ultimately, updating it to a persistent store.



Last month you learned how to display read-only XML data in Internet Explorer 5+ (IE), but not how to modify and save the data. Browser security settings typically prohibit you from saving data on the client. So the problem is: How do you modify the data and make it persistent?



Using ADO or DOM methods you can make your data islands do the hula, for example, users can add, modify, or delete records and then persist them locally. For documents under 64K in length, you can persist them using IE’s userData behavior. You can persist larger documents locally using a signed ActiveX control.Laying the Ground Work
To recap, displaying XML data in IE is easy. You place an XML data island inside your Web page using IE’s proprietary element, bind form fields to the fields in the data using the data source object (DSO), and write some simple JavaScript functions that call the ADO MoveNext and MovePrevious methods.

The ease with which you can display XML data in IE lies in the fact that you can treat the data as either a Document Object Model (DOM) object or an Active Data Objects (ADO) record set. The DOM is an abstraction of how objects contained within an XML document relate to each other. In essence it is a tree whose base is the root node. You’re probably already familiar with the DOM in HTML, where the root node is the document object. You reference all other objects/nodes on an HTML page through the root node — the document object. When IE encounters an XML file, it builds a tree of node objects that you can reference in much the same way as HTML objects — by using DOM methods to refer to a specific object/node in the tree. After you have a reference to the correct object/node, you can call DOM methods to manipulate it. This month, I’ll show you how to update, add and delete records using both ADO and DOM and two different methods to save your data to the client’s machine.

Last month I showed you a small phone book application. This month, I’ll expand on that by using a much larger phone book XML document containing data for all the full time faculty at Norwalk Community College. There are over 250 records, which scales the problem up to reasonable real-world levels for an application of this type (view a read-only demo version). Except for the Save and Delete buttons, the code is very similar to last month’s code (see Listing 1).

The code in Listing 1 contains a

You'll use those elements during the save process later in this article.Updating Records
Updating records is easy because you don't need any scripting whatsoever. IE hides all the plumbing involved in using the data island component, it is responsible for keeping the display coordinated with the data. For users, this automatic synchronization makes updating records a snap--just type a new value into any bound data field in the associated form and move off the record. That's it! The bound form field's new data replaces the data in the XML document.

Of course, it couldn't be that easy! As you found out in last month's article, at this point, the XML data exists only in memory?not in a persistent store. Therefore, even though users can see their changes to the data by navigating through the record set, reloading the page reveals that nothing has really changed; the data island still contains the original data from the phone.xml file specified by the src attribute of the element. Because the underlying file remains unchanged, the page always displays the original data. From a developer viewpoint, IE's automatic synchronization between the display and the in-memory copy of the data is not the same as automatic persistance. When you need persistance, you must implement it yourself.Adding Records
Open up your text editor and create a JScript file containing the code to enable the navigation structure for the application. Save the file as xmlSave.js in the same folder as your XML document. and then save it as xmlSave.js (this code is identical to last month's xmlNav.js, so if you already have that, you can just copy the file):

   var currentRecord = 0;      function first(){         data.recordset.moveFirst();         currentRecord = 0;      }            function previous(){         if(data.recordset.absoluteposition>1) {            data.recordset.movePrevious();            currentRecord--;         }      }         function next(){         if(data.recordset.absoluteposition <             data.recordset.recordcount){            data.recordset.moveNext();            currentRecord++;         }      }         function last(){         data.recordset.moveLast();         currentRecord = data.recordset.recordcount - 1;      }
Using ADO to add records is as easy as navigating through them. Add the following to the bottom of the file:

   function addRecord(){   //ADO Implementation   data.recordset.AddNew();   phoneForm.iName.value = "--Last Name, First Name--";   phoneForm.iExt.value = "--phone--";   phoneForm.iRoom.value = "--location--";   phoneForm.iEmail.value = "--e-mail--";   phoneForm.iNotes.value = "--notes--";   last();   }
The addRecord() function creates a new row, and then sets default values for the new row. The call to the last() function moves the record set pointer to the newly added row. Users can enter new data, move off the record, and then move back to verify that they've successfully added a record.

A better way to add new records is to use the DOM. One easy way to add new records is to include a second data island in the page that points to an XML file that specifies default values for each field. Open your text editor again and create newRecord.xml with the following code:

               --Last Name, First Name--      --extension--      --e-mail--      --room--      --notes--   
The structure of newRecord.xml is identical to that of a single record in data.xml. Now, add a second data island to the Web page (I added mine directly beneath the first data island):

   
Next, modify the addRecord function in xmlSave.js:

      function addRecord(){      //DOM Implementation - uses second data island      var insRecord = newRecord.XMLDocument.documentElement.cloneNode(true);      data.XMLDocument.documentElement.appendChild(insRecord);      last();   }
The DOM implementation first clones the newRecord data island. The documentElement property represents the root element, so the clone occurs on everything in the newRecord.xml file. The code creates a new node--insRecord?that contains the cloned document and appends it to the end of the main data island. Finally, the code moves the current record pointer to the newly created record, which (because the HTML document contains bound fields) causes IE to display the new record.Deleting Records
Once again, using ADO to delete records is easier than writing the same functionality using the DOM. Add the following code to xmlSave.js:

   function deleteRecord(){      //ADO Implementation      data.recordset.Delete();      if(data.recordset.absoluteposition<0){         //if you deleted the last record,          //move to the "new" last record         last();      }      else         //do nothing, IE will move to the next record   return;   }
That's it! The Delete() method deletes the current record and,if you are not on the last record, moves on to the next record. If you delete the last record, the record set pointer is moved to the "new" last record. The DOM implementation doesn't offer any great benefit over the ADO implementation but, in the interest of completeness, I offer it here. Modify the deleteRecord() function as follows:

   function deleteRecord(){      //DOM Implementation   var delRecord =    data.XMLDocument.documentElement.      childNodes(currentRecord);      data.XMLDocument.documentElement.         removeChild(delRecord);      if(data.recordset.absoluteposition<0){         //if you deleted the last record,          //move to the "new" last record         last();      }      else         //do nothing, IE will move to the next record      return;   }
The DOM implementation stores the current node in a local variable, then deletes the node from the data tree. It's only one more line of code so choosing a method to delete records is largely a matter of taste.

Persisting Data
Saving the updated XML data tree is absolutely necessary in any real world application. In all honesty, when I first began writing this article, I couldn't conceive of any reason to save an application like this on the client. Saving the data to each individual client would result in different versions of the phone book on each machine. Then, a few weeks ago, I received the latest edition of my local phone book from the phone company. It dawned on me that everyone gets the latest snapshot of the phone book that we are free to customize by writing in the margins etc. Saving the XML data to the client mirrors this process.

The data island component exposes an XML property that returns the XML string that makes up the data island. I'll make use of this property to grab the contents of the XMLDocument and persist the data in a couple of ways, each of which has its own limitations.Saving Records?a Brute Force Approach
Normally, scripts running in the browser do not have access to the client's local file system. IE, however, has the ability to execute ActiveX controls which, when given permission, can access the local file system. You can make use of IE's built in FileScriptingObject to save files locally. Create a save() function in your script:

   var saveType = "ud";   function save(){      var xmlData=phoneForm.savedData;      if(saveType=='fso'){      //FSO Implementation      var forWriting = 2;      var dataFile =      "c:\phone.xml";      var ts;      var fso =new   ActiveXObject          ("Scripting.FileSystemObject");      ts = fso.OpenTextFile(dataFile,forWriting,true);      ts.Write(data.xml);      ts.Close();      //these lines clear the userData cache      // so the code will load only the       // latest saved version      xmlData.value="null";      xmlData.setAttribute         ("persistedData",xmlData.value);         xmlData.save("savedXML");      }   }   
The preceding code sets a few properties, instantiates the FileSystemObject, and then opens the underlying XML file as a writeable text stream. The third parameter indicates that the open method should create the file if it doesn't already exist. Passing the XML from the data island to the text stream's Write() method causes the data stored in memory to overwrite any data stored on disk. Closing the text stream flushes the buffer ensuring that the stream writes the data to the file. Finally, the code clears the userData cache so the page will retrieve the newly saved file correctly the next time the user views the page.

There are some obvious and potentially sinister shortcomings to the ActiveX approach. ActiveX controls require permission from the user to do their work. IE fires up an ominous message recommending that the code not be run at all (see Figure 1).

You have three choices. The first two are to either reconfigure the client's browser settings so that it will run ActiveX controls without prompting (use IE's security settings to mark the site as trusted -- suitable for an Intranet application), or instruct the user to click "Yes" to execute the code. Marking the site as trusted creates a wide open security breach, because it gives any ActiveX control from that site carte blanche to create, copy, move, or destroy information on the local machine. Instructing users to let the code execute trains them to let other potentially unsafe ActiveX code run as well?something you should avoid out there on the wild, wild Web. The third possibility is to create a digitally "signed" ActiveX control. IE will ask the user if they want to install and run the control the first time the page is visited but subsequent visits will be seamless as long as the control remains installed. You can find out how to digitally sign an ActiveX control here.

The hard coded file path is another problem with the ActiveX control approach. Hard coding paths is almost always a bad idea, because sooner or later you'll run into a situation where the path is invalid. If you leave out the path info, Windows saves the file to the user's desktop, which is not a palatable option.Saving Records?A More Elegant Approach
IE 5 introduced a persistence framework that allows authors to specify an object to persist data--called userData?on the client using DHTML behaviors. The persistence framework lets you save the state of specified objects on a page. I'll make use of the built-in userData persistence mechanism to save the XML document in a hidden form field of the display.

The userData persistence mechanism requires three things:

  • A style sheet rule that applies the behavior
  • An object whose data will be saved
  • JavaScript functions that write the data to the client machine.
Here's the style sheet rule for the sample project:

   

You specify the element to save by assigning a class attribute just as you would any other CSS rule:

      
Finally, modify the save() function to write to the userData area on the client:

   function save(){      //userData implementation      var xmlData=phoneForm.savedData;      xmlData.value = data.xml;      xmlData.setAttribute("persistedData",    xmlData.value);      xmlData.save("savedXML");   }
The save() function first creates a local variable that references the hidden form field. Next, it assigns the value property of the form field variable to the XMLDocument stored in memoryto the. The setAttribute() call associates the value of the form field with a key ("persistedData" in this case). The last line saves the form field to an XML file called "savedXML.xml" stored in the client's userData area. On Windows 95/98, IE stores user data to:

   C:WINDOWSApplication DataMicrosoftInternet    ExplorerUserData
On Windows NT and 2000, IE stores user data to the user's Documents and Settings folder in the path:

      Documents and SettingsusernameApplication    DataMicrosoftInternet ExplorerUserData. 
On all Windows machines, the main UserData folder contains sub-folders that hold all the userData that saved on that machine.

There is one big downsideto the userData mechanism--there's a size limitation on the amount of data that can be stored on the client. The amount of space available varies depending on where the document resides. Microsoft's documentation contains conflicting reportsabout the space available for locally persisted data; some sources report 64K, others, 128K.

My Windows 98 machine running IE 5.0 rejects documents larger than 64KB. The phone.xml file I used is initially 40K, which doesn't leave much room for customization by the user. Simply put, if your XML files are larger than 64KB, you'll have to break them up into smaller pieces and save them separately, save them on the server, or bite the bullet and deal with ActiveX object permissions?but you won't be able to use the userData cache.Will the Real Data Please Stand Up
The userData persistence method saves the value of the hidden form field; however the data island gets its data from the phone.xml file. You'll need to write another function that loads the saved data into the data island instead. The tricky part is that you want to load the server-based phone.xml file the first time a user loads the page, but on subsequent loads, you want to load the persisted data. The loadData function fulfills that requirment. Add the function to the xmlSave.js file call it from the body element's onLoad event:

   function loadData() {      var xmlData=phoneForm.savedData;      xmlData.load("savedXML");      xmlData.value=xmlData.getAttribute         ("persistedData");      //if not the first time, check userData first      if((xmlData.value)!="null") {         data.loadXML(xmlData.value);      }      else    {         //then check for the file         var dataFile = "c:\phone.xml";         var forReading = 1;         var ts, s;   var fso = new ActiveXObject       ("Scripting.FileSystemObject");         if(fso.fileExists(dataFile)) {            ts = fso.OpenTextFile(dataFile,forReading);            s = ts.readAll();            alert(s);            xmlData.value = s;            data.loadXML(xmlData.value);         }         else {            // If you made it here, this is the first view            // or there's no locally saved data.            // Do nothing, use the server version            return;         }      }   }
The preceding code is almost a reflection of the save() method. First, the code retrieves a reference to the phoneForm's savedData input field. Next, it retrieves the savedXML file from the UserData cache, retrieves the. persistedAttribute key and assigns its value to the form field's value. When the user first loads the page, or if the user has never saved the data, the persistedAttribute key value is "null" and the page populates the data island with the data from the phone.xml file on the server. If the cache contains data, then the call to the loadXML() method loads the data island with the XML stored in the cache. If the cache does not contain data, the application uses the master data from the server.

To test the application, load the page xmlDataDemoWithSave.html into IE. Add a new record, and then click the Save button. Close IE and then reopen it, navigate to the xmlDataDemoWithSave.html file again and move to the last record. The new record you created will reappear.

While this XML data island application is far from complete, you should have a good handle on how to display and manipulate XML data from within IE5. You can see how saving XML-formatted user data provides far more power and flexibility than using cookies. At the very least, you can see that the XML capabilities of IE5 are both flexible and powerful.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: