Export (0) Print
Expand All
Expand Minimize

ASP.NET ListEditor in C#

 

Chris Lovett
Microsoft Corporation

July 25, 2000

Editor's note: You can now download the .NET Framework SDK Technology Preview, which includes the C# compiler. Please note that this is an early technology preview and a large download.

Download Listeditor.exe.

I had an absolute blast at the Microsoft Professional Developer's Conference (PDC) in Orlando last week. What a thrill to be able to stand up in front of thousands of the industry's best software engineers and show them what we've been busily working on for the past few years. I didn't sleep a wink the night before my talk. I was really impressed with the level of questions we got afterward and was grateful for the positive feedback!

The funny thing is that this new .NET Platform is one of Microsoft's best-kept secrets ever, and the media hasn't even caught on yet. They still think .NET is just vaporware. Those who attended the PDC know the real story. There is a ton of amazing, innovative work in the .NET Framework—and it has been in development for years. I've personally been programming in C# for about a year now, and I love it! It is definitely the most productive programming environment I've ever used—and I've used just about every environment that has been on the market.

We have a brand-new, 100 percent C# (C-sharp) implementation of the XML family of technologies. This includes stream-level classes, such as XmlReader and XmlWriter; W3C DOM classes such as XmlNode, XmlDocument, XmlElement, and so forth; XPath classes, such as XmlNavigator; and XSLT classes, including XslTransform.

You'll see a lot of overview papers and reference material on all this stuff, so I won't drill into it here. Instead, I've decided to live up to a promise I made at the PDC, which is to port the ListEditor application from my April article to C#, running in the new .NET ASP+ Framework. See the ASP+ and C# source code.

Now, because I already designed this application to be loosely coupled between client and server using XML as my data-transfer format, the client side of the application remains unchanged (except for the .asp filenames changing to .aspx).

The fact that I can simply plug in a new server-side implementation is yet another example of why the loosely coupled Web architecture works so well.

The XML that comes down is the list that I am editing on the client, and the XML that goes back is made up of little XML "updategrams," like this one:

<updates schemastamp="8">
 <update type="modify" location="//item[id='102']" by="clovett" name="description">
  <old></old>
  <new>Fine-grained notification of changes in the tree</new>
 </update>
</updates>

Porting to ASP+

I did a straight port of the original ASP JScript® code to ASP+ C#. I could have done a lot more—redesigning the server-side code to use all the new .NET Web services, ADO+, and so forth—but I decided not to do this yet, because I wanted to show you how easy it is to plug in to the new .NET Framework from your existing Web applications.

The porting effort went smoothly, and because C# is a strongly typed language, I actually found and fixed a number of bugs in my original code. Before, the editor couldn't handle lists that stored the data in attributes at all; this is now fixed. I also modified the ListEditor so that it no longer lets you edit the schema in a way that results in invalid XML. Previously, people could easily break the XML by putting bogus element names in the schema. The new version is a lot more robust. It also will not let you delete the required ID element any more.

ASP+ compiles the C# each time I change it, so it works exactly the same way as the old ASP model. You typically see a little delay (1 or 2 seconds) the first time it compiles the page after you've made some changes, but subsequent requests are snappy.

I also found a couple new bugs in the new XML classes, which is to be expected when you're using "Technology Preview" bits. Fortunately, the bugs had easy workarounds. You'll see "BUGBUG" comments in the code to this effect.

I then decided to tidy up my code somewhat by moving all the real functionality in the ListEditor pages into a new C# class called "ListEditor." This class has the following public methods:

public class ListEditor
{
    public ListEditor(HttpApplicationState app);
    public string GetUserName(HttpRequest Request, HttpResponse Response);
    public string GetNextId(string filename, string where);
    public string Update(string filename, TextReader updategram);
    public string UpdateSchema(string filename, TextReader updategram);
    public string Sync(string filename, string timestamp, string xsl);
}

The ASP+ pages are now all skinny wrappers that delegate to this class. For example, the save.aspx page looks like this:

<%@ Page Language="C#" Src="ListEditor.cs" %>
<%@ Import Namespace="System.Collections" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.NewXml" %>
<%@ Assembly Name="System.XML.dll" %>
<%
    Response.Expires = -1;
    Response.ClearContent();
    ListEditor editor = new ListEditor(Application);
    Application.Lock();
    string result;
    try {
        result = editor.Save(
            Server.MapPath(Request.QueryString["file"]),
            new StreamReader(Request.InputStream));
          } catch (Exception e) {
            result = editor.FormatException(e);
          }
    Application.UnLock();
%>
<%=result%>

The GetUserName function just encapsulates the authentication code, which looks basically the same as the original ASP code. This time, I provided a way to bypass authentication by specifying the username as a URL parameter. For example:

http://clovett4/listeditor/edit.aspx?file=feedback.xml&user=test

The GetNextId function returns the next unique ID to use when inserting a new item into the list or schema. This function is called from the nextid.aspx page:

http://clovett4/listeditor/nextid.aspx?file=feedback.xml&where=/test

The code for the function looks like this (with the critical pieces in bold):

public string GetNextId(string file, string where)
{
    XmlDocument curdoc = new XmlDocument();
    curdoc.Load(file);
    XmlNavigator nav = new DocumentNavigator(curdoc);
    if (nav.SelectSingle(where)) {
        int next = Int32.Parse(nav.GetAttribute("next"))+1;
        string value = Int32.Format(next,"d");
        nav.SetAttribute("next", value);
        curdoc.Save(filename);
        return "ok " + value;
    }
    return "error: node not found";
}

First, the code above loads up the list that is being edited into an XmlDocument object (in this case, "feedback.xml"). The XmlDocument object is the new C# version of the World Wide Web Consortium (W3C) Document Object Model.

Next, the code wraps this XmlDocument object with an XmlNavigator class. It uses an XPath expression to find the node containing the next attribute, which it then increments. Then it saves the document back to disk, and returns the new ID to the client.

XmlNavigator

XmlNavigator is an important new class in the .NET Framework, so I want to take a moment to explain it. XMLNavigator provides a "cursor" or "iterator" over any XML tree, and methods for moving around the tree, such as the following:

  • MoveToNext moves to next sibling.
  • MoveToPrevious moves to previous sibling.
  • MoveToParent moves up the tree.
  • MoveToFirstChild moves down the tree.
  • MoveToAttribute moves into the attribute axis.

The XmlNavigator class can also modify the tree with methods, including:

  • Insert inserts new nodes into the tree.
  • Move moves nodes around the tree.
  • Remove removes nodes from the tree.
  • CopyChildren— I lost count of how many people asked for this one!

The XmlNavigator class also provides rich support for dealing with XPath expressions. In this example, we used the SelectSingle function, which is the new managed equivalent of the MSXML selectSingleNode method.

Other methods for XPath expressions provided by the XMLNavigator class are:

  • Select the equivalent of the MSXML selectNodes method.
  • Evaluate returns a number of string result from an expression.
  • Matches returns whether the current node matches the expression.
  • Compile compiles an expression so you can use it over and over.

For example,

int total = (int)nav.Evaluate("count(/list/item)");

would return the number of item elements in the list.

Now, you're all probably saying to yourselves, "This is all good stuff, but why a cursor? Why not just extend the DOM with these methods?" The answer is simple. An XmlNavigator class can navigate any XML, not just the in-memory DOM tree. For example, we have a sample XmlNavigator class that navigates the system registry. It is simply not practical to populate an in-memory DOM tree with the entire contents of the registry, because the DOM doesn't scale. The DOM requires "object identity for nodes"—which means that every time you ask for a node from the DOM, you are supposed to get back the same object. This makes it hard to virtualize. The XmlNavigator class solves this problem—and we have in fact already exploited this by providing an XmlNavigator subclass called DataDocumentNavigator, which can navigate relational tables, rows, and columns stored in an ADO+ DataSet.

The XmlNavigator class's XPath support also allows you to provide input to the XslTransform class. Plug in the DataDocumentNavigator, and you will quickly see that you can now transform relational data with XSLT just as easily as you can transform the contents of an XmlDocument object. Very cool!

Back to ListEditor. There is some effort involved in switching your code over from the DOM to the XmlNavigator model, as when I ported this ListEditor over. I found it easy to do—and, because of convenience methods, such as CopyChildren, my code actually got smaller.

Let's look at another function. The Update function calls the following Insert function when the updategram is of type="insert".

private string Insert(XmlNavigator doc,
                      XmlNavigator schema,
                      XmlNavigator update)
{
    string id = update.GetAttribute("id");
    schema.SelectSingle("//ElementType");
    string name = schema.GetAttribute("name");
    string idname = "@id";
    if (schema.SelectSingle("element[@type='id']"))
        idname = "id";
    XmlNavigator doc2 = doc.Clone();
    string query = "//" + name + "[" + idname + "='" + id + "']";
    if (doc2.SelectSingle(query))
        return "id " + id + " already in use";
    query = update.GetAttribute("before");
    TreePosition pos = TreePosition.LastChild;
    if (query != null && query != "") {
        if (doc.SelectSingle(query))
            pos = TreePosition.Before;
    }
    doc.CopyChildren(pos, update);
    return "ok";
}

For those of you who have looked at the original source code, this will appear familiar. The only significant difference is the use of the CopyChildren method. You will also note that the above code is not inserting white space. The new XmlDocument class formats the XML for readability when you Save the document.

XmlDocument

Let's look at another piece of code—this time, some DOM code. Like the original ListEditor, the new version stores the updates in a shared document in ASP Application state. The Save.asp and SaveSchema.asp pages append to this shared document each time an update is received. The new C# code for this is:

private void AppendLog(XmlDocument updates, string schemastamp,
                       string timestamp)
{
    XmlDocument log = this.UpdateLog;
    XmlElement logroot = log.DocumentElement;
    logroot.SetAttribute("schemastamp",schemastamp);
    logroot.SetAttribute("timestamp", timestamp);
    foreach (XmlNode update in updates.DocumentElement) {
        if (update.NodeType == XmlNodeType.Element) {
            XmlElement newnode = log.ImportNode(update, true);
            newnode.SetAttribute("timestamp", timestamp);
            newnode.SetAttribute("schemastamp", schemastamp);
            logroot.AppendChild(newnode);
        }
    }
}

Notice that the new XML Document Object Model is nicely integrated into the C# programming language, so that you can use the foreach statement over the children of an XmlNode class. Notice also that (as defined in the W3C DOM spec) to get nodes from one document object to another, you need to use the ImportNode method.

Conclusion

C# is easy to think about when you're familiar with JavaScript programming. I found myself typing in "var name = ..." quite a bit until I eventually got used to writing "string name = ...". Other than that, it was a natural transition.

The new .NET Framework and C# are real. In a short amount of time, my new C# version of the ListEditor far exceeded the old version in every way. The biggest challenge was holding myself back from writing tons more code, tidying up more, adding new features, and making more use of the cool new .NET Framework.

The ListEditor is still designed for low volume. The performance bottleneck is actually the file system, because it is loading, updating, and saving the XML lists on each ASP+ request. Instead, it should cache stuff in memory, or use a real database backend. But the ListEditor will really shine if you are doing low volume edits on a million lists.

Chris Lovett is a program manager for Microsoft's XML team.

Show:
© 2014 Microsoft