|
|
Client ExpectationsIt’s often said that one of Microsoft’s biggest competitors are the previous versions of it products. In our case as ASP/XML developers, the problem we face is that over the years our customers and colleagues have come to take for granted user interface patterns that they expect to see in any application we write today, regardless of the product we use to develop those applications. Applications that fail to meet these expectations are like toothpaste manufacturers who continue to use tubes with screw-on caps: nobody is impressed. The spreadsheet metaphor is perhaps the most ubiquitous of these UI patterns, but close behind are more granular patterns like drop down boxes, dynamic user interfaces where changing one field automatically changes others. Functionality like sorting (implemented by clicking column headings) and filtering are also taken for granted by our customers. But we as developers can’t take them for granted. In the next ninety minutes I want to walk you through the growing collection of user interface patterns I’ve built. Of course by the time these written remarks are actually spoken, only God knows what else I will have discovered, so I reserve the right at this writing to change the patterns I will now discuss. As we get started then, keep remembering that we’re at the beginning of a very important milestone in the development of personal productivity, social change, and human development. But keep in mind that since supply exceeds demand, talk is cheap, and the world doesn’t want another prophet, it wants the real thing. So let the transcendent encourage you, but stick close to the knitting and, work with me to provide the development community with cool tools with which to change the world – one user at time. Essential User Interface PatternsIn their seminal paper on user interface design (a paper that is very important to my third talk, “Encapsulated XML”) Krasner and Pope wrote, “…tools that were put into the [Smalltalk system] interface tended to consist of arrangements of four basic viewing idioms: paragraphs of text, lists of text (menus), choice “buttons,” and graphical forms…These tools also tended to use three basic user interaction paradigms: browsing, inspecting, and editing.” JOOP, Aug/Sep 1988, pp. 26 In one way or another, all three of my presentations focus on one or the other of these two categories; my first two talks, “Essential XML” and “XML at Work,” focus on the category of “browsing, inspecting, and editing.” My third talk, “Encapsulated XML,” talks more about the controls themselves (the paragraphs, lists, buttons, and graphics) and that third talk presents a programming technique to coordinate an arbitrarily complex user interface. Essential XML-based User Interface PatternsThe central user interface pattern in this talk is the XML-based spreadsheet. Actually, there are two different implementation strategies for the spreadsheet. One is based on a given XML file; the other is a polymorphic spreadsheet that will morph itself around any given ADO 2.5 recordset. Both styles of spreadsheet support the second tier of UI patterns including sorting and filtering data. Sorting in both directions is supported as is the “circular filter” whereby moving forward through a data set to its end returns the pointer to the beginning of the data set with one more press of the forward button (and likewise for moving backwards through data). Use of the alt key implements both multidirectional sorting and filtering to the beginning or end of a dataset. The third tier of UI patterns includes devices like drop down boxes containing distinct values from a database projection. From this list users can filter a dataset down to single values. It should be noted in passing, however, that you could avoid this particular device by exploiting the inherently hierarchical nature of XML. I’ll cover both the drop down box and the hierarchical tree alternative in my second presentation, “XML at Work.” Two other handy user interface devices are context menus (which I cover below) and drag ‘n drop frameworks (which I leave for another time). The Spreadsheet MetaphorI think it’s fitting I’ve selected the spreadsheet UI pattern to begin my discussion of Essential XML because it’s the spreadsheet that launched the personal computer revolution over twenty years ago. The first spreadsheet is hardwired to its XML data store, and the polymorphic spreadsheet uses the schemas provided by ADO2.5 and XSL to bind any data on the Internet to the spreadsheet pattern. The Spreadsheet with StyleThis section is based on an XSL Spreadsheet article Wrox originally published in June, 1999. I will not duplicate in this talk what I wrote in that article. I will, however, clean up the code previously published, and I will add one important feature: strongly-typed attribute data. XML could handle strongly-typed element data in Windows2000/Beta3, but not until Release Candidate 1 could you use attributes with equal facility. This is welcome news since ADO persists XML using attributes. Updating XMLI made one other change to the original XSL-based spreadsheet. I moved the implementation of the data updating from the database server to an XML file stored directly on the file system. My preliminary work at Microsoft, directed at solving the XML “authoring” problem, suggests that – at least for the short-term – much XML data will be stored in separate files. More than that, these files will be manipulated directly by programs, not users. And finally, at runtime, these separate files will be brought together, much like SQL Server creates sets of data from separate tables, and displayed as if they were a single file. So updating XML files on the file system is a key goal for the effective use of any XML-based document management system (see my closing comments below). Here’s one way to do update file-based XML: function getXML() { if (!deltaBatch.load(Request)) // get the incoming batch changes from XMLHTTP throw deltaBatch.parseError.reason; else // open the underlying xml file dataStore.load(Server.MapPath("budgetXML3.xml")); } function postXML() { var strResult=''; deltas = deltaBatch.selectNodes("xml/*"); // for each row of incoming changed data while (delta = deltas.nextNode()) { // get the row of old data whose primary key was passed in var data=dataStore.selectSingleNode(_ "xml/row[@title_id='"+delta.getAttribute("title_id")+"']"); // update the field data.setAttribute("ytd_sales",delta.getAttribute("ytd_sales")); strResult += 'Updated record with:\n'; strResult += 'TITLE_ID: '+data.getAttribute("title_id")+'\n'; strResult += 'Title: '+data.getAttribute("title")+'\n'; strResult += 'New YTD_SALES: '+_ data.getAttribute("ytd_sales")+'\n\n'; } // save the XML file (note there are no concurrency controls here) dataStore.save(Server.MapPath("budgetXML3.xml")); Response.write(strResult); } One final remark: the advent of XHTML means that even well-formed HTML files will become “databases” in their own right. This has got to be keeping database administrators all over the world up all night. Oh, and one other comment about XHTML: I have converted all the XML tags to lower case. Since lower case is mandated by the XHTML spec, I’m getting in the habit now. Editing XMLOf course there’s nothing to update if nothing is edited. The spreadsheet discussed in this talk only has one editable field. This field must communicate several important data points to the program. It must display the YTD_SALES field’s data; and it must register the name, id, and value of the current selection. It must inform the program when the cursor has moved and when a cell has been edited; and it must tell the application when the context menu is invoked. Here’s how it does all these things. <td class="yStyle"> <input name="ytd_sales" class="yStyle" onblur="display(editPending);" onchange="updateXML(this);" oncontextmenu="handleContextMenu()" onfocus="cacheOriginalValue()" > <xsl:attribute name="id"> <xsl:value-of select="@title_id"/> </xsl:attribute> <xsl:attribute name="value"> <xsl:apply-templates select="@ytd_sales"/> </xsl:attribute> </input> </td> In this stylesheet I’ve hardwired the name of the cell, but I bind the id attribute (it’s the primary key from the data store) and the value attribute at runtime. Let’s examine how this cell behaves in each of two scenarios. First, when the tab key is pressed to move down the spreadsheet the display() event handler is called and the value of the editPending variable (false by default) is passed in. when editPending is false display() does nothing important (I often use this moment to display something about the state of the system through the status window in the lower left corner of IE). The display() function is also called when the program starts and by the sorting and filtering functions; in each of these latter cases, the display() function’s argument is set true so the stylesheet fires to re-render the spreadsheet. See the ASPToday XSL Spreadsheet article for details. The other context where cell movement does not include editing is when the onfocus event fires. The cacheOriginalValue() event handler simply assigns the key attributes about the current selection in an object, selCell. The process used to control the edit process, to which we now turn, needs the information from selCell to determine when the user has changed their mind. So, let’s see what behavior is triggered in the application when a cell is changed and the tab key is pressed. The event handler for onchange, updateXML(), is a bit more complicated. function updateXML(strInput) { // set global var true so that the onblur event will refresh the // display, but only on the first tab (subsequent tabs won’t refresh) editPending=true; editedRow=strInput.parentElement.parentElement.rowIndex; // get the current xml record; deltasRow needs the title attribute var sourceRow=source.selectSingleNode(_ "/xml/row[@title_id='"+strInput.id+"']"); // update this attribute in the underlying XML sourceRow.setAttribute(strInput.name, stripValue(strInput.value)); // update derived value, net // use this algorithm: // net = advance - ( price * ytd_sales * royalty / 100) var x = sourceRow.getAttribute("advance") - _ ( sourceRow.getAttribute("price") * _ sourceRow.getAttribute("ytd_sales") * _ sourceRow.getAttribute("royalty") / 100); sourceRow.setAttribute("net",x); // Look for the deltas node with the passed in primary key var deltasRow=deltas.selectSingleNode(_ "/xml/row[@title_id='"+strInput.id+"']");
if (null==deltasRow) { deltasRow=deltas.createElement("row"); deltasRow.setAttribute("title_id",strInput.id); deltasRow.setAttribute("title",sourceRow.getAttribute("title")); // store this field's original value. Later, if the user reverts, // we won't persist the edit. deltasRow.setAttribute("originalValue",selCell.value); deltas.documentElement.appendChild(deltasRow); } // see if user has reverted this node back to its original value // compare with ClickContextMenu() event handler if(stripValue(document.all(event.srcElement.id).value)==_ stripValue(deltasRow.getAttribute("originalValue"))) // delete this delta, this cell's value hasn't changed deltas.documentElement.removeChild(deltasRow) else // update this attribute in deltas XML deltasRow.setAttribute(strInput.name,strInput.value); } This function does two main things. It updates the underlying XML data to reflect the new value entered in the cell. It also keeps track of these changes in a separate XML tree called the “deltas” tree. When the edited cell is first edited, a new node is added to deltas. Subsequent edits of the same cell will find that cell id in deltas, and so updateXML() checks to see if this “new” value is the same as the original value. If it is, the current node in deltas is deleted. The context menu provided for this editable column also manages this reversion process -- explicitly. Context MenusInternet Explorer 5.0 introduced the oncontextmenu event. This new event handler gives developers the chance to display something other than the standard context menu displayed by Windows. To make this work you need to attach the oncontextmenu event to a control and you need to disable the standard context menu. You also need to ensure that once displayed, the context menu gets credit for any mouse clicks; if you don’t, then the context menu will remain displayed. Context menus are simply div tags whose absolute position is set at runtime based on the location of the mouse click. The div tag displays itself and processes all mouse events. Once a selection is made, the div tag makes itself invisible and returns control of the mouse to the application. In the process, the div tag does whatever it was told to do by the menu selection. In my case, there is only one element of the menu, but it’s enough to suggest how to make more complex menus do your bidding. Here are the two functions that make the menu work. function handleContextMenu() { with (DivContextMenu) { innerHTML = "Revert to original value"; style.position="absolute"; style.width=150; style.zIndex=200; style.border="outset 2"; style.fontFamily="microsoft sans serif"; style.fontSize="9pt"; style.padding="6pt"; style.backgroundColor="menu"; style.posLeft=event.clientX+document.body.scrollLeft; style.posTop =event.clientY+document.body.scrollTop; style.cursor="default"; style.display=""; setCapture(); } event.returnValue = false; return false; } function ClickContextMenu() { DivContextMenu.style.display="none"; DivContextMenu.releaseCapture(); if(deltasRow=deltas.selectSingleNode(_ "/xml/row[@title_id='"+selCell.id+"']")) { var sourceRow=source.selectSingleNode(_ "/xml/row[@title_id='"+selCell.id+"']"); // replace the displayed value gridBody.all(selCell.id).value=_ deltasRow.getAttribute("originalValue"); // restore the original value to the xml tree sourceRow.setAttribute(selCell.name, _ deltasRow.getAttribute("originalValue")); // remove the delta node so we don't update deltas.documentElement.removeChild(deltasRow); } } The bold lines are the key ones to remember. You ensure the context menu controls all mouse events with the setCapture() method, and you release this control with the releaseCapture() method. Note in the handleContextMenu event handler I use the with JScript keyword so I don’t have to type DivContextMenu more than once. Finally, you disable the appearance of the standard Windows context menu with event.returnValue = false command. Advancing the CursorThe next UI pattern is the smallest, but a small size doesn’t mean a small impact on your users. Indeed, isn’t it the “little touches” that often have a magnified effect on how you feel about using a piece of software? These two functions are called by display(). function highLightCell() { if (gridBody.rows.length==editedRow) editedRow=0; gridBody.rows[editedRow].all.ytd_sales.focus(); selectCell(); }
function selectCell() { var tr=document.selection.createRange(); tr.moveEnd("textedit",1); tr.select(); } So when the last cell in the displayed grid is edited (i.e., when gridBody.rows.length==editedRow) the spreadsheet cycles back up to the first cell of the grid once the spreadsheet is refreshed. The selectCell() function is standard DHTML code for selecting text on the screen. In my case, I need the whole cell highlighted so I use the “textedit” argument for the moveEnd() method of the TextRange object. Communicating with the ServerThe final processing to discuss is how the spreadsheet persists changes to the XML data store. Here there are two things to do: maintain the deltas tree of edited cells and post the XML to the server for processing. If the user presses the Post button without making changes, the postXML() function catches that and tells the user there is nothing to update. If there is data in the deltas tree then postXML() checks to see if the user is holding down the Ctrl key; if so, this tells postXML() to clear out the tree. Otherwise, the data is sent to the server via WinInet using the XMLHTTP component. This marvelous invention, this XMLHTTP component, has dramatically affected the way we design three tier web-based client-server applications. The postXML() function, for example, can send an xmldom object directly to the server without first converting it to a BSTR (a string class invented to allow C++ programs to communicate with Visual Basic or scripting engines). By the same token, the server can return an xmldom directly to the client employing the same savings in processing. I need nothing but a text message from the server, however, and so I display it to the user to confirm that the edit went without a hitch. Had there been an error on the server, the alert dialog would have displayed the error message. Once the edit cycle is complete, the deltas tree is pruned back to the trunk to be used for another batch of updates. A Tip or TwoWhenever we increase the complexity of our applications we increase the complexity of the troubleshooting required to fix problems. Hopefully, these two factors don’t scale linearly, and are less than strongly correlated. The design strategy I discuss in “Encapsulated XML” is designed with this goal (of improving power faster than problems) in mind. But sometimes complexity just creeps up on you. Adding strongly typed attribute data is one example of added complexity that doesn’t respond well to clever design strategies. There are, however, a few tips you can keep in mind when you encounter issues related schemas (a prerequisite to strongly-typed XML – and a topic I don’t have room to detail). The first trap you can fall into is thinking you are validating your XML against your schema…when you’re not. When this happens, the nodeTypedValue will have the nodeValue datatype (usually string). When you then try to sum what you think are floats together you get, instead, a big string with (perhaps) multiple decimal points. This is bound to cause unexpected results including a runtime error in the stylesheet. Stylesheet runtime errors are notoriously hard to troubleshoot because we currently cannot get debuggers attached to the XSL parser (so here’s another tip: when you get stylesheet runtime errors copy the function into the html SCRIPT section and debug it there). The best way to get out of this trap of the invalid validation assumption is to open the XML file with a validating parser or a tool like XMLPad. Just deliberately invalidate and save your XML, and when you open the XML file with XMLPad you will know immediately whether validation is working. If it’s not, you probably didn’t add the xmlns attribute necessary to attach a schema to XML data. Here’s the correct syntax: xmlns=” [the url to your schema]” It is all too easy to forget the x-schema: prefix, and when you do, no validation is taking place. I should warn you, however, that at this writing there is a problem with XMLPad: when you save, or sometimes merely open, a file (usually when the schema namespace is declared) you may get the xmlns=”” attribute attached to each of your XML file’s nodes. The problem is easy to correct, just global search and replace that attribute with nothing. This problem will be fixed by the time you read this, but you will need to download the latest version of XMLPad from the URL given above. At this point I should clarify a statement
in made in the XSL
Spreadsheet article. I said, “I need to reference the Instead, remember this: when validation is disabled the dataType and definition properties return null. The nodeTypedValue property returns the same value as the nodeValue property (again, usually type string). For example, dates without validation return “2000-08-14” (note, you need ISO 8601 date syntax for XSL), and with validation return 08/14/00 with “date” as the dataType property (and yes, that looks like a potential Y2K problem – by the time of the conference I should have the details on this two digit year). At any rate, you can see that you will raise a runtime error when an XSL function like formatDate() expects a date type but gets a string (see my comments below) or when you try to sum a series of “numbers” with total+=r.nodeTypedValue; for total+=will concatenate strings instead of summing numbers. Here’s one other little insight that might come in handy. When it comes to dates, XSL is especially intolerant of validation violations. If you merely have a template that matches on a date field and the template does nothing at all, XSL will still throw a runtime error. It is interesting that XSL will process an <xsl:apply-templates select="@pubdate"/> tag without complaint, but as soon as a matching template even sees it, the XSL parser will handle the exception and halt the stylesheet. One final (for now) point: I have also discovered that, contrary to the documentation, the XSL function formatNumber() will coerce a string to a double. This is why <xsl:eval>formatNumber(this.nodeTypedValue, "#,##0")</xsl:eval> still works even when validation is disabled and this.nodeTypedValue is a string. While I’m on the subject of debugging XSL, permit me to pass on a few other observations. First, the debugger can’t reach inside an XSL file. Even stylesheets that contain client-side code can be a little tricky. For the same reason, adding breakpoints to source code won’t work. Therefore, I suggest you place the debugger or stop (if you prefer to program XML with VBScript) keyword in the client-side code you want to watch and do NOT attach the IE process through VID’s Processes dialog. Simply let the debugger come up in a separate process. Once you see the familiar pair of debugging dialogs you’ll be running the client-side code in the debugger. The Polymorphic SpreadsheetReturning to my spreadsheet metaphor, let me bring the technical part of this talk to a close with a brief overview of the polymorphic spreadsheet. For convenience I will refer to the polymorphic version of the spreadsheet as the “grid.” I have used my XML-based spreadsheet in several different applications, and each time I had to tweak the code to match the data. I am inherently lazy so I set out to generalize my spreadsheet. Ideally, I could fetch any ADO2.5 recordset and display the data in a spreadsheet with the correct headings and knowledge of the columns’ data types so that sorting and filtering could be done on the data and totals could be computed. The ado3.xsl stylesheet is the current state of the art (and I underscore art). Creating the Table HeadingsTo create the grid.htm file I moved in all of the client-side script from budgetShow3.xsl. The getData.asp file provides grid.htm with all its data simply by opening an ADO connection to my local SQL Server and fetching a bunch of records from the familiar titles table in the pubs database. The one thing getData.asp does differently is use ADO 2.5 ability to persist XML to the IStream interface. The innovations that generalize the grid are left in ado3.xsl. Let’s take a look at them now. First, the stylesheet creates table headings by processing the schema tag ADO sends with every recordset persisted to XML. This schema doesn’t exactly conform to the XML-data schema spec (probably) because it was implemented in ADO before the XML-Data spec was finished. The ADO schema does, however, provide ado3.xsl with enough meta-data about the recordset that I can reach almost all my goals. Here the stylesheet loops through the schema node (note the xsl pattern in bold) and applies a template to process each node according to the node’s datatype: <xsl:for-each select="xml/s:Schema/s:ElementType/s:AttributeType" > <TH><xsl:apply-templates select="s:datatype"/> <xsl:value-of select="@name"/> </TH>
<xsl:eval>copySchema()</xsl:eval> </xsl:for-each> The called template returns the <xsl:attribute>tag containing the correct ONCLICK event handler function call given the data type reported for each column in the recordset. Inside the TH tag will appear the name of the field. The copySchema() function creates an associative array with the name of the field/attribute as the name of the array element and the datatype from the schema as the corresponding array value. The schema[] array is used by the style(), displayTotal(), and render() custom stylesheet functions (see details below). I had to do this because I gave up trying to come up with an xsl expression that mapped a node in the schema node to a node in the data node of the XML. Creating the Table BodyNext, I need to dynamically create the body of the table that will display the XML. Here’s the XSL that does that (note the XSL pattern in bold that iterates through the collection of rows). <xsl:for-each select="xml/rs:data/z:row" order-by="nothing"> <xsl:if test="context()"> <TR class="data"> <xsl:for-each select="@*"> <TD> <xsl:attribute name="align"><xsl:eval>style()</xsl:eval></xsl:attribute> <xsl:eval>render()</xsl:eval> </TD> </xsl:for-each> </TR> </xsl:if> </xsl:for-each> The style() function looks to the associative array produced by copySchema() and returns the align attribute’s value given the data type of the current cell. In the same way the render() function looks up the same field name and formats the field value given its datatype. Finising up with the Table FooterFinally, I need to figure out which fields are summable. Here the XSL (in bold) iterates through all the attributes of each row updating an associative array (by field name) used by displayTotal(). <TFOOT id="gridTotals"> <xsl:for-each select="xml/rs:data/z:row/@*"> <xsl:eval>sum(this)</xsl:eval> </xsl:for-each> <TR> <xsl:for-each select="xml/s:Schema/s:ElementType/s:AttributeType" > <TD align='right'> <xsl:eval>displayTotal(this)</xsl:eval> </TD> </xsl:for-each> </TR> </TFOOT> The displayTotal() function returns to the schema node for the name of each attribute in the grid. By referring each name to the array, displayTotal() can reference the total value of each attribute and return each total to the table in the proper format. Where’s Waldo?I’m tempted to come clean on some of the finer bugs…uh, points in this grid application, but I think it would be more fun for interested users to critique this prototype yourselves. I will ask for the resume of the one(s) who comes up with the most bugs and will pass it on to Microsoft recruiters to help fill current requisitions for positions on the App Server Test Team. Send your email to mailto:mcorning@microsoft.com. Some Final Thoughts: The Next LevelIt occurs to me that the combination of an inherently hierarchical (and queryable) data structure, user interface metaphors more powerful than tables, and latently hierarchical documents portend an important evolution of thought about how we approach our documents. Put another way, in the same way that Word serializes subsections (that is, subsections are not nested in Word files), we humans think of documents two-dimensionally was well. Put another way, still, I see a day (soon) where we treat our documents the way we treat the objects in our programs. When this day comes we will erect the structures of our documents before we begin to actually write them. We will erect these structures primarily with a drag ‘n drop paradigm and with a tree metaphor, not a table. When this happens we must necessarily think differently about our documents, and about how documents are (latently, at first) linked together (both structurally and semantically). XML then becomes even more important than it is today, filling yet another key role in a technology that enables us to move up to higher levels of abstraction about those abstract things we put to paper (or screen). SummaryThis presentation makes the case that there are certain user interface patterns that have evolved over the last twenty years and that computer users take these patterns for granted. Any application that doesn’t meet these expectations can expect resistance from users and a slower (if any) rate of adoption. XML is an emerging technology, one many of us are just beginning to learn. Applications built on XML, then, may not have the same look and feel and the same polish of its older brothers and sisters; but this need not be so for long. This talk has shown how some of these user interface patterns can be implemented in an XML application. Along the way I have suggested that XML’s unique attributes enable it to do things with the implementation of user interface patterns that is difficult (if even possible) using previous technologies. Finally, I have suggested that this first step is just that, a first step, toward the next world of writing and reading. A world presaged by the Web, but a world that can now take full form through the querying and semantic modeling power of XML. This talk lays the groundwork for my third presentation (my second talk being a diversion into some real blue collar XML) where I begin to explore a programming technique, the Model-View-Controller design framework, that is perfectly suited to this new structural and data processing power of XML. In the immediate future I intend to come back to these spreadsheets and apply MVC to them, too. This will be especially important when I start adding more UI patterns to the basic spreadsheet metaphor. These additional features include adding and deleting rows, editing multiple columns, and supporting hierarchical data (including an XML-based pivot table). Indeed, since we are only in the earliest stages of building XML-based applications, perhaps one of you will join me in this project. There is much work to do, and no single (not even a single group) can do it alone. Success will come only by the concerted effort of the entire XML/ASP development community. I hope you join me. |
|
|
|
|
|
| |
Information Online swimming pool builder chicago web site design online fax Cheap Web Hosting online faxing Giorgio armani sunglasses answering service |
Email TopXML
|
|
Front Page Daily Stuff TopXML Forum XML blogs XML Newsgroups BizTalk Biztalk Utilities Biztalk Utilities Tutorial B2B SAP XML Microsoft .NET Dotnet System XML Soapformatter SQLXML XMLserializer XQuery PHP PHP SimpleXML PHP XML Dom PHP XML RPC PHP XSLT Java Java Java XML Xalan Microsoft ASP ASP Schemas XML SQL Server XML XMLDom XSL XSL Tutorial XSLT Stylesheets General Javascript CSS XHTML WAP |