Skip to main content

Adventures in Jscript: Episode Three - XPath and While to the Rescue

An Introduction:

Last week we saw just how easy it is to use a nifty combination of Jscript and XPath to "replace" client XSL Transforms. Before we get into it, I'd thought I'd clear up a couple of comments from last week. First, I'm not opposed to XSL on the client. It solves many problems that are a pain in Jscript...like sorting to mention one. However, since you have to jump out to a different language and maybe even a different development environment, I think it's easier to just write it all in Jscript. And remember: XSL only gets you the display. Any binding code you need has to be done in Jscript, and again, I think it's easier to keep all of your code in one place. So hopefully that has answered the inquiries / accusations that I was somehow anti-XSL...on to Episode 3.

Serialization, Whiles and XPath...

Last week I mentioned serialization in the context of my XML. A question popped up in my inbox that made me realize that I didn't do a good job covering that..so here goes. When I speak of XML serialization, I'm really referring to the act of serializing an object into XML. The object in this case is a recordset but it could be a more complex custom object (and that's why we banter about SOAP, and that's a whole 'nother Oprah..). Most of us have tables of data living in a database that we need to display on the client. We extract that data out into a recordset (of some sort) and then we have a routine of some kind that cranks through the recordset, creating a big string. That big string happens to look like XML and that's what we'd call a serialized recordset. Some apps serialize in the database, some in the middleware and some of us still do it on the web server in ASP. The end result is a string of known format (XML) that can be squirted down to a browser client in the Response stream. For my purposes in the sample pages, I'm imbedding the XML on the HTML, so they are sort of "post-serialization". But remember, I could just as easily be building these in ASP, Response.Write-ing the content between the <XML> tags. To the client, how the serialization happens doesn't matter, it's what the XML looks like that is important.


On the "Monster", we have an exotic blend of Visual Basic 6 COM+ objects that do most of our serializing. Our complex middleware has to span two different RDMS's, Oracle and SQL Server and performs a lot of magic in the process of serializing. Even with their magic, the XML doesn't look as simple as what we saw in Episode Two. In the database, the data looks something like this:


Primary KeyFirst NameLast Name
43MikeShaffer
65MarthaShaffer
23RitaShaffer
7MatildaShaffer

Last week we arbitrarily created unique control names (txtFirstName1, txtFirstName2...) so we could satisfy HTML's requirement that Id's be unique for page elements. But let's imagine that the people crafting the serializing don't want to keep track of this naming convention so they're going to label everything txtFirstName. This works for them, but obviously messes us up big time. We need to figure out how to use the primary key. My proposed XML would look something like this:


<top>
<row pk="43">
<field name="txtFirstName" value="Mike" originalval="Mike" />
<field name="txtLastName" value="Shaffer" originalval="Shaffer" />
</row>
<row pk="65">
<field name="txtFirstName" value="Martha" originalval="Martha" />
<field name="txtLastName" value="Shaffer" originalval="Shaffer" />
</row>
<row pk="23">
<field name="txtFirstName" value="Rita" originalval="Rita" />
<field name="txtLastName" value="Shaffer" originalval="Shaffer" />
</row>
<row pk="7">
<field name="txtFirstName" value="Matilda" originalval="" />
<field name="txtLastName" value="Shaffer" originalval="Shaffer" />
</row>
</top>

But this doesn't solve our unique HTML element issue, does it? My solution is that since we're dynamically building the display table, we'll grab the pk attribute value on the <row> tag, and append the control name with that. As a rule I don't display the database's primary key to the user, unless it's like a customer number or something so our display table will appear "keyless" to the user. We'll have something that looks like this:


<td><input type="text" id="txtFirstName_43" value="Mike" /></td>


Our init() function will look something like this:


var CurrentRowPK;
while((oOuterNode = oOuterNodes.nextNode()) != null)
{
CurrentRowPK = oOuterNode.getAttribute("pk");
sString += "<tr>";
var oInnerNodes = oOuterNode.selectNodes("field");
var oInnerNode;
while ((oInnerNode = oInnerNodes.nextNode())!=null)
{
switch (oInnerNode.getAttribute("name").substr(0,8))
{
case "txtFirst":
sString += "<td class='First'>"
break;
case "txtLastN":
sString += "<td class='Last'>";
break;
}
sString +="<input type='text' id='" + oInnerNode.getAttribute("name") + "_" + CurrentRowPK + "' value='" + oInnerNode.getAttribute("value") + "' onchange='TextBoxChange();' /></td>";
}
sString += "</tr>";
}

I grab the pk value off of every <row>, then tack it on to the id value, creating a unique id for HTML. Now we have to modify our TextBoxChange() function. The only change is modifying our XPath, which is the power of this scheme. With a radically different XML structure, we can quickly modify our creation function and our bind function. Our basic function has this added to it:


var a_Name = who.id.split("_");
var Row = a_Name[1];
var TheNameThatChanged = a_Name[0];

We now quickly load the id of the control (like txtFirstName_43) into an array, splitting it on the "_". Then we know that the value in array position 1 has just the <row> pk. We also put the name of the control, array position 0 into TheNameThatChanged. Now we modify the XPath:


var xp = "//row[@pk='" + Row + "']/field[@name='" + TheNameThatChanged + "']";

What XPath is doing is now performing a multi-level query, looking first for a <row> that matches the pk value, then looking for a <field> of that with a name attribute matching TheNameThatChanged. Take a look at Example 1


So that should be it, right? If only. What if (and this really happened..) the serializers come back and say "That XML is too hard to build...Here's what we want to give you." Do I quit and run screaming from this bunch of whiners? No, I suck it up and think "Anything is imminently do-able.". Here's their XML:


<top>
<row>
<field name="PrimaryKey" value="43"/>
<field name="txtFirstName" value="Mike" originalval="Mike" />
<field name="txtLastName" value="Shaffer" originalval="Shaffer" />
</row>
<row>
<field name="PrimaryKey" value="65"/>
<field name="txtFirstName" value="Martha" originalval="Martha" />
<field name="txtLastName" value="Shaffer" originalval="Shaffer" />
</row>
<row>
<field name="PrimaryKey" value="23"/>
<field name="txtFirstName" value="Rita" originalval="Rita" />
<field name="txtLastName" value="Shaffer" originalval="Shaffer" />
</row>
<row>
<field name="PrimaryKey" value="7"/>
<field name="txtFirstName" value="Matilda" originalval="" />
<field name="txtLastName" value="Shaffer" originalval="Shaffer" />
</row>
</top>

And if I walk a bit in their shoes (and I have, believe me, I have..) I can see their point of view. When you retrieve data from the database, the recordset usually looks like the above table: the Primary Key is a cell in every row. You can imagine a very simple function that cranks through all rows in the recordset, then going through each cell in the row, building the XML string. Getting the Primary Key and putting in the <row> is technically "harder". I'm not biased here, I don't think it's that big of a deal, but it makes for good TV..... So how do we handle this? Just like before, we modify our init() function to build the display slightly different, then we modify the XPath in our TextBoxChange() function. First the init() function changes:


var CurrentRowPK;
while((oOuterNode = oOuterNodes.nextNode()) != null)
{
sString += "<tr>";
var oInnerNodes = oOuterNode.selectNodes("field");
var oInnerNode;
while ((oInnerNode = oInnerNodes.nextNode())!=null)
{
if(oInnerNode.getAttribute("name") == "PrimaryKey")
{
CurrentRowPK = oInnerNode.getAttribute("value");
}
else
{
switch (oInnerNode.getAttribute("name").substr(0,8))
{
case "txtFirst":
sString += "<td class='First'>";
break;
case "txtLastN":
sString += "<td class='Last'>";
break;
}
sString +="<input type='text' id='" + oInnerNode.getAttribute("name") + "_" + CurrentRowPK + "' value='" + oInnerNode.getAttribute("value") + "' onchange='TextBoxChange();' /></td>";
}
}
sString += "</tr>";
}

The first thing that is obvious when you run Example 2 is that the table looks exactly the same, both it's visible and HTML source. That's a good thing. We shouldn't have to burden the user with an interface change just because some VB hack can't make XML the way you want it.


I've introduced a little bad form that I feel obligated to disclose. I am assuming that the field nodes are in a specific order, PrimaryKey, FirstName, LastName. If someone were to reorder these, it'd be a tragedy. OK, maybe not quite that bad but the table wouldn't render at all and that would be bad. From a purist stand-point, I should design so that the order of the XML is not known, making it very flexible. However, I abandoned purity from a design standpoint a long time ago and since I know that the serialization will always look like this, I'm safe. Your experience may vary. It wouldn't be too bad to redesign things to not assume this field order....but I'll leave that to you.


Now to redesign our TextBoxChange() function. The only change is to change the XPath. Here's how it will look:


var xp = "//row[field[@name='PrimaryKey' and @value='" + Row + "']]/field[@name='" + TheNameThatChanged + "']";

We've got a lot more going on here. The important concept here is that XPath is performing a subquery. Literally it is saying "find the row/field with a name of TheNameThatChanged that is a sibling to the row/field with a name of 'PrimaryKey' and a value of Row". In SQL you do subqueries all the time. This is what makes XPath so powerful, you can drill into the XML document and get and set values.


To be fair, there is another way to do this. It's a little more tricky.... The first thing you need to do is make sure your data island has it's SelectionLanguage set to XPath. You'd think this would be by default, but no such luck. When you do the above queries, it's not necessary. But for the following it is...go figure. You do this like:


TheIslandXML.setProperty("SelectionLanguage", "XPath");

I usually stick this in my init() function, but as long as it's done before you perform the query, you're good. Next we build a similar, but different XPath:


var xp = "//row/field[@name='PrimaryKey' and @value='" + Row + "']/parent::*/field[@name='" + TheNameThatChanged + "']";

If you've done some good old DOM manipulation in DHTML, this might look familiar. Literally, we're going down onto the PrimaryKey node, then moving to the parent (back to the row) then down to the field. Same result as before, but with a slightly different XPath. See Example 3.


Would you choose one XPath over the other? Not that I can determine. I've seen no performance advantage of either. Syntactically, they are about the same. The only real difference is that with Data Islands you have to set the SelectionLanguage property to use parent::*(and previous-sibling::* and a bunch of other, similar statements). If you use a different method to create your XML objects and can guarantee MSXML4, this isn't even necessary. (Now if Microsoft would let us "choose" the XML version for the Data Island, that'd be cool...). So I'm not going to recommend one over the other, it's kind of a MacDonalds vs. Burger King thing...your choice.


An Aside:


I've snuck in another bit of functionality that bears explanation. On every change, I add an "action" attribute with a value of "change" to the <row>. Why I did that is another bit of "Monster" legacy, but how I did it is actually a bit cooler. First the "why"... Our application is constantly updating and inserting and deleting stuff, it's a data entry system after all. The middle-tier coders, who ultimately shove the data into the database, needed an easy way to designate the "action" being performed on the "row". Clever naming convention. So the "Monster" UI code keeps track of whether this is an Update, Delete or Insert and marks the action accordingly. My true feelings were that since we drag around this original value attribute on every field, you could compare the two: if original is blank and value is not, it's an insert. If it's the opposite, it's a delete. If value is different than original, it's an update. But after countless hours of debate, it was determined that my solution would not be robust enough so I ended up having to code a bunch of junk to determine the "mode" we're in, and mark the action accordingly. Such is the life of an interface designer... Now the "how": MSXML DOM nodes have a .setAttribute method, we've used to change the values of the <field> nodes. The real cool thing about .setAttribute is that it will create the attribute for you if it doesn't exist already, kind of like Expando's in Jscript. So you can take the source XML and enrich it to your heart's content. .setAttribute is powerful medicine but very safe...doesn't error on you. Just be careful that the attribute value your setting matches exactly or it'll create a new, slightly different version. Just like Jscript (and all good stuff IMHO), it's case sensitive. "value" is different than "Value"..... I'm also using the .parentNode method. MSXML DOM nodes have a parent, and this just bumps us up a level, very quick without a lot of hassle.


The astute reader of this rag will notice that previously I referred to Microsoft's implementation of Javascript as JScript. I am now calling it Jscript. I was cruising through MSDN the other day and noticed that Microsoft refers to it in this fashion. So that's my story. I have noticed that even they are not 100% consistent, so I feel glad in guaranteeing my lack of consistency as well.

The Finish Line:

I think we've covered some good ground, the while loop and XPath combo are foundational concepts in all of my interfaces. Now, we'll move on for a look at behaviors, an IE specific improvement to this whole wacky web browser world. We'll start with standard behaviors next week.


If anyone has any comments or questions, email me at ms_@_mybluecoat.com (remove the underlines first...) or use the nifty comment deal that Foo has added to AlphaFilter...and keep those cards and letters coming.

Popular posts from this blog

A teacher's credentials...

Kristie told me this funny story..... The School has been blessed for years with a teacher who has dutifully taught yearbook class to the Senior High students. She abruptly resigned a couple of weeks ago, for personal reasons and nothing to do with the School or the job per se. In a mad scramble, the principal scrounged up a "suitable" replacement. This replacement was described as "a mom". Fast forward to yesterday. Kristie was doing new employee orientation and this new hire was present. Kristie did emphasize this person seems to be a terrific individual and an all around nice lady. When Kristie introduced herself she stated: "So you're the new yearbook teacher....nice to meet you". The new hire replied, "Thanks, do you know anything about this yearbook stuff? Because I don't..." Kelly signed up for yearbook this year...she already has Digital Photography, a questionable class and now Yearbook. Wednesday Elective Day is going t...