Gray's Matter
Justice Gray - North America's favorite metrosexual software consultant

I Wish These People Updated More Than Once a Year

We resume our saga from the previous chapter, where the three amigos are going through some growing pains in coding the Autosave for DasBlog 1.9...
DasblogAutosavePanel03.jpgDasblog Autosave panel 03: Spider-man: 'Coming in to steal my thunder, Jesus?  What makes you more special than Spider-Man??   I don't see *you* sticking to walls and shooting webs!' Jesus: 'Well, I...' Denzel: 'Lay off Jesus, Spider-Man!!  We can use all the help we can get on this autosave.' Jesus: 'Denzel, how many times do I need to tell you...I can fight my own battles!!!  For once, let me take care of this myself.  Now step off!' Denzel: 'Sorry, Jesus.  Stepping off now.
I think EDMUG started somewhat like this

In the constructor to the ajaxdelegate, we store all of the values in an Array (which will be passed to the callback object).

this.callbackArguments = new Array();
this.callbackArguments[URL] = url;  
this.callbackArguments[CALLBACK] = callback;  
this.callbackArguments[ENTRYID] = entryId;  
this.callbackArguments[TITLE] = title.replace(/]]>/g, "]]>");  
this.callbackArguments[AUTHOR] = author;  
this.callbackArguments[POSTTEXT] = postText.replace(/]]>/, "]]>");  

[Spider-Man: 'All right, Jesus...I'll cut you some slack for now, but you've got to prove yourself to me.' Jesus: 'I've heard that one before somewhere...' Denzel: 'TITLE and POSTTEXT seem to have some funk going on.  What is that about?' Jesus: 'That's because we're using CDATA tags for storing those values.  CDATA tags instruct the XML parser to treat this as character data and not markup!' Spider-man: 'Always have to be the know-it-all,don't you, Jesus?  What a jerk!!'

As Jesus points out, later on in our Fetch command we're constructing a small piece of XML data like so:

var xmlSubmit = "<savedpostdata><entryid>"+callbackArguments[ENTRYID]+"</entryid>";
xmlSubmit += "<title><![CDATA["+callbackArguments[TITLE]+"]]></title>";
xmlSubmit += "<author>"+callbackArguments[AUTHOR]+"</author>";
xmlSubmit += "<posttext><![CDATA["+callbackArguments[POSTTEXT]+"]]></posttext>";
xmlSubmit += "</savedpostdata>";                
request.open("POST", this.url, true);
request.setRequestHeader('Content-Type', 'text/xml');
request.send(xmlSubmit);

You might be thinking that this isn't a big deal, but the XML parser that needs to read this data will *break* on any occurence of the &nbsp; character (of which there will be many, unless you like writing your posts without any spaces).  That's just one potential problem Jesus and Denzel are avoiding by wrapping these values in a CDATA tag.  

Now let's get back to that weird-@$$ed callbackArguments initialization code.  There is still one way the XML parser can break: if we insert a "]]>" character sequence somewhere in the text of either the title or the HTML content of the post text itself.  You see, CDATA tags are bordered with "<!CDATA[" on the left side, and "]]>" on the right side.  If we threw an extra "]]>" in the middle of this, we'd end the CDATA segment early and pass malformed XML to the AutoSaveEntry handler.  Thus, we use a regex that looks for any instance of "]]>" and escapes it to "]]&gt;", &gt; being the escape sequence for the right angle bracket (greater than).  While it's very likely that the user is only typing this sequence when making a typo or a deliberate attempt to break the system, it's better for us to be safe than sorry.

In the request, we set our request method to "POST" (as we really don't want to be shoving this in the query string), and set our 'content-type' to 'text/xml', so that the request knows what format this should be sent in.  And then we are off!!!

[Before we go on, the exciting part of something like this is that you can actually accomplish the remainder of our auto-save with *any* sort of server-side language that allows some sort of writes to storage (read: pretty much all of them).  DasBlog uses XML to store its entries, but you could just as easily use PHP, Java, or whatnot to read our XML and store it somewhere.  The remainder of this text goes into our ASP.NET HttpHandler specific solution.]

Jesus: 'I don't need to take this, Spider-Man! Aside from being a master haxx0r, I have also come back from the *dead*.' Spider-Man: 'Big deal!  I've come back from the dead too!!  Just check Spider-Man #17, 1991.' Jesus: 'Yes, but you're a *fictional character*, Spider-Man.' Denzel:  'HA!!  You tell him Jesus!!
Jesus Christ pwns the n00bs

After this exchange, the group bands together and whips up a brief HttpHandler that would take incoming requests and use those requests to save data.  The first step: to define a new class called AutoSaveHandler, that implements the IHttpHandler interface.  After this, they put an entry in the web.config to ensure that any requests to autosaveEntry.ashx were handled like so:

<add verb="*" path="autoSaveEntry.ashx" type="newtelligence.DasBlog.Web.Services.AutoSaveEntryHandler, newtelligence.DasBlog.Web.Services" />

Now we didn't get any failures when hitting "autosaveentry.ashx", but it still isn't doing anything.  So they add some simple code to the ProcessRequest() method.

First, we read the XML data received from the AjaxDelegate.  We use an XmlTextReader since it's less overhead than the XmlDocument class, and we only need forward read-access anyway.  

System.Xml.XmlTextReader xtr = new System.Xml.XmlTextReader(context.Request.InputStream);
            try
            {
                while (!xtr.EOF)
                {
                    xtr.Read();
                    if (xtr.Name=="entryid")
                    {
                        entryId = xtr.ReadInnerXml();
                    }
                    if (xtr.Name=="author")
                    {
                        author = xtr.ReadInnerXml();
                    }
                    if (xtr.Name=="title" && xtr.NodeType == System.Xml.XmlNodeType.Element)
                    {
                        xtr.Read();  // Brings us to the CDATA inside "title"
                        title = xtr.Value;
                    }
                    if (xtr.Name=="posttext" && xtr.NodeType == System.Xml.XmlNodeType.Element)
                    {
                        xtr.Read();
                        textToSave = xtr.Value;
                    }
                }
            }
            finally
            {
                xtr.Close();
            }

Once we've read all of these values into their respective variables, we save our entry, provided that our entryId value isn't null or empty.  There are two cases we need to take care of here.  If this is a new entry, we need to create a brand new entry object, assign it our parameters, and call the SaveEntry() method of the DataService.  If it's a previously edited/autosaved entry, we get the existing entry, change its Modified time, and save it.  

Entry entry = dataService.GetEntry(entryId);
if ( entry != null )
 {
     entry = dataService.GetEntryForEdit( entryId );
     Entry modifiedEntry = entry.Clone();
modifiedEntry.Content = textToSave;
modifiedEntry.Title = title;
modifiedEntry.Author = author;
modifiedEntry.Syndicated = false;
modifiedEntry.IsPublic = false;
modifiedEntry.ModifiedUtc = DateTime.Now.ToUniversalTime();
modifiedEntry.Categories = "";
dataService.SaveEntry(modifiedEntry, null);
}

                else // similar code, but creating a new entry instead.

Whew!!  With this in place, we now take a large circle back to our Javascript callback function AutoSaveResult() (mentioned in part 1), which does absolutely nothing but write a small note to the label we inserted under the textbox, saying "Post auto-saved: [current date and time]".  Two notes here:
the "post auto-saved" part is actually localizable; we're grabbing it from a variable called autoSaveText which has been set to the value in our .NET localization tables like so:

string autoSaveText = resmgr.GetString("text_autosave");

sb.Append("function AutoSaveResult(url, response)  " + "\n");
sb.Append("{ \n");
sb.Append("var dt = new Date(); \n");
sb.Append("document.getElementById('" + this.autoSaveLabel.ClientID + "').innerHTML = '" + autoSaveText + " ' + dt.toLocaleString(); \n");
sb.Append("} \n");
sb.Append("</script> \n");


With all of this in place, the Autosave is ready to run!!  Thanks to the triumphant trio for figuring this out!  The sad truth, however, is that due to the conspiracy of silence surrounding DasBlog, I don't think it'd likely that we'll see a "Thanks to Spider-Man" from Scott Hanselman anytime soon.  But our group has moved on to other projects, as you can see...

Spider-Man: 'Well, Jesus, I have to admit it, you sure worked a miracle on *this* project.' Denzel: 'No doubt!  You were basically the Savior of Autosave!!' Jesus Christ: 'Thanks Denzel and Spider-Man.  I'm glad I could make a believer out of you guys, and glad that we've added to DasBlog!  We make a good team!  So what's next for us?' Spider-Man: 'I could use some help with the Green Goblin if you guys aren't too busy!!' Jesus Christ: 'We're on it, Spider-Man!!  Green Goblin, Venom, Dasblog, Mysterio - they don't a chance against Jesus Christ, Spider-Man, and Denzel Washington!'
And so great teams are born...

Friday, 25 August 2006 #