2005.07.12 08:39 PM

FlexWiki Notes - JavaScript Issues, Part 1

The FlexWiki JavaScript that builds and displays a dynamic menu of topic links to choose from when hovering over a topic link having multiple possible matches doesn't work in FireFox. The fix for this is pretty easy, and I'll describe the necessary changes in my next post. However, before making those changes, I felt it necessary to change how the JavaScript was being sent to the page in the first place.

The reason I felt this way was because the JavaScript in question (most of it, anyway) was embedded as a very long string in the WriteScripts method of the FlexWiki.Formatting.HTMLWikiOutput class. That's a drag. It meant that everytime I needed to change the JavaScript, which was often as I debugged it for FireFox, I had to modify and recompile the entire FlexWiki engine. This was not a model for rapid application development. Ideally, the JavaScript would reside in its own file in the web application's root folder and be referenced using the src attribute of the HTML's script element instead.

Without looking too closely at HTMLWikiOutput, this seemed to be an easy thing to do. Unfortunately, it wasn't. I'll try to explain why and describe the solution I ultimately adopted.

Let's assume that the JavaScript has been pulled from the HTMLWikiOutput.WriteScripts method and written to a file named HTMLWikiOutput.js in the web application's root folder. Then a first attempt at a new WriteScripts method that returns a script element that references this file might look like this:

void WriteScripts()
{
  Write(@"<script type=""text/javascript"" src=""" + "HTMLWikiOutput.js" + @"""></script>");
}

Much shorter than the original. Unfortunately, it doesn't work. The reason has to do with how FlexWiki handles topic request URLs. Take a look at the following topic URLs:

http://localhost/wiki/default.aspx?topic=MyNamespace.HomePage
http://localhost/wiki/default.aspx/MyNamespace.HomePage
http://localhost/wiki/default.aspx/MyNamespace/HomePage.html

The first one represents an old format that's no longer supported by FlexWiki (see the FlexWiki.Web.BasePage class GetTopicName method and the commented out version below it for more details). The second is a new format based on the now deprecated old format. And the last one is the new recommended format.

If FlexWiki still used the first format, the WriteScripts method contemplated above would work, because the browser would establish the relative path for the script element's src as the portion of the URL prior to the requested resource (i.e., default.aspx), leaving just "http://localhost/wiki/". However, to the browser, the new formats look either like folder requests (in the first case) or resource requests in a folder beneath the web app's root (in the second case). So even though IIS is smart enough to peel off the portion of the URL following the requested resource (i.e., default.aspx) and launch the proper page (making the remaining portion of the URL available to the page's code via the HttpRequest.PathInfo property; again, see the FlexWiki.Web.BasePage class GetTopicName method for details), the browser is left thinking that the request represents a deeper path than the web app's root and will establish the relative path for the script element's src as "http://localhost/wiki/default.aspx/MyNamespace.HomePage/" or "http://localhost/wiki/default.aspx/MyNamespace/".

There are a number of ways to solve this. The first obvious approach would be to modify the WriteScripts logic above to include an absolute path. However, closer examination of the HTMLWikiOutput class shows this to be harder than expected. To help explain why, I prepared the following extremely condensed and barely legible (non-standard) UML sequence diagram depicting the primary path through FlexWiki for topic requests:

FlexWiki uses an instance of the FlexWiki.LinkMaker class everywhere to prepare resource URLs. The URLs it prepares are alway based on the the web application's root path, which is passed to the LinkMaker's constructor during instantiation. In general, this is a good thing. The diagram shows how the BasePage class creates and holds an instance of LinkMaker during PageLoad, initializing it with the path resulting from a call to its RootURL method. This LinkMaker object is passed as a parameter to the FlexWiki.Federation constructor, where it is held for the object's lifetime (which can be pretty long, as it is subsequently cached in HttpApplicationState and reused by subsequent requests).

After this initialization, default.aspx calls its DoPage method, which writes to the Response object the results of a call to the Federation object's GetTopicFormattedContent method. This method returns the results of a call to the static method FormattedTopic in the FlexWiki.Formatting.Formatter class. Note that the Federation object passes the LinkMaker it got during instantiation as a parameter to the FormattedTopic method. This is important, as the Formatter class needs a properly initialized LinkMaker in order to build all sorts of properly path'd anchors in the resulting HTML.

Here's the problem. At some point, the FlexWiki developers, bless them all, decided it would be a good thing to separate the logic for creating topic output from the class that interprets topic mark-up (Formatter). This is a good thing. They did this by introducing the abstract base class FlexWiki.Formatting.WikiOutput. This class acts as a factory for producing WikiOutput-derived classes via its static ForFormat method based on the enum OutputFormat. As delivered, FlexWiki provides only one complete WikiOutput-derived class, HTMLWikiOutput (hence, the diagram above shows all calls passing the OutputFormat.HTML enum).

As an aside, I'll mention that I think there's only a slim chance anyone will be creating another non-HTML WikiOutput-derived class soon, at least not without a lot of work. The reason is that the separation of the output logic to WikiOutput-derived classes is only about half done (there's a comment in Formatter that implies it is 90% done, but I think that dear soul was being insanely optimistic). There are still many places in Formatter that assume the output is HTML (for instance, see the Emote method, which uses LinkMaker directly to generate img elements).

Anyhow, the trouble for us is that these same FlexWiki developers did not think it necessary to pass to the WikiOutput-derived classes the Formatter's LinkMaker during initialization. As a result, it's not clear how the WriteScripts logic above can be modified to include the web application's root path in the script element's src attribute. Where's it going to come from?

So many choices. Let's explore the least desirable ones first, as they're also the easiest.

One approach would be to shift path determination to the client. This has the advantage of restricting our changes to the WriteScripts method, impacting no other parts of the FlexWiki engine. The disadvantage is that it's an ugly hack. It involves sending JavaScript to the client to dynamically add a properly path'd script element to the document during page loading:

void WriteScripts()
{
  Write(@"
          <script type=""text/javascript"">
            var scriptPath = document.location.href.substr(0, document.location.href.length - (document.location.pathname.length + document.location.search));
            scriptPath += '/' + document.location.pathname.split('/')[1] + '/HTMLWikiOutput.js';
            document.write(""<scr"" + ""ipt type='text/javascript' src='"" + scriptPath + ""'></scr"" + ""ipt>"");
          </script>
  ");
}

Yuck. But, hey, if you're into that sort of thing, go for it.

Another approach would be to leverage something learned from the diagram above. Specifically, note that the Federation object initialized during the first request, which holds a reference to a LinkMaker, also created during initialization, was tucked away into the application cache. Why not use it?

void WriteScripts()
{
  string scriptPath = ((Federation) System.Web.HttpContext.Current.Application["---FEDERATION---"]).ExposedLinkMaker.SiteURL() + "HTMLWikiOutput.js";
  Write(@"<script type=""text/javascript"" src=""" + scriptPath + @"""></script>");
}

Oh, I know why, because it introduces a backward-facing dependency from the FlexWiki engine to FlexWiki.Web and hardcodes a repeated cache key string. Yuck. But, hey, it works.

Another approach would be to repeat the RootURL method logic found in BasePage, relying on the fact that the FlexWiki engine is executing within ASP.NET and, therefore, has access to the Request object via the HttpContext. In short, we could get and tear apart the Request.Path ourselves (kind of like the first approach, but on the server). Heck, it could even use this information to spin up its own LinkMaker instance, just like BasePage. But, really, this is all just too nasty to contemplate.

The right thing to do, I believe, is bite the bullet and modify the WikiOutput class to take a LinkMaker via its ForFormat factory method and hold it in the instances of its children. This works better in the long run, particularly if I ever decide to finish separating the remaining 10% (chuckle) of Formatter's HTML output logic, then I'll have ready access to the LinkMaker for path'ng img and anchor elements.

Here are the modified bits of WikiOutput:

public static WikiOutput ForFormat(OutputFormat aFormat, WikiOutput parent, LinkMaker lm)
{		
  switch (aFormat)
  {
    case OutputFormat.HTML:
      return new HTMLWikiOutput(parent, lm);

    case OutputFormat.Testing:
      return new TestWikiOutput(parent, lm);

    default:
      throw new Exception("Unsupported output type requested: " + aFormat.ToString());
  }
}

WikiOutput _Parent;
LinkMaker _LinkMaker;

public LinkMaker LinkMaker
{
  get
  {
    return _LinkMaker;
  }
}

... instance property members ...

public WikiOutput(WikiOutput parent, LinkMaker lm)
{
  _TextWriter = new StringWriter();
  _Parent = parent;
  _LinkMaker = lm;
}

Note that I use a property to provide LinkMaker access, similar to Federation, whereas the Formatter class uses a method. Whatever.

By the way, did I ever mention how much I dislike abstract base classes mixed with static factory methods? Am I the only one who finds this ugly and confusing?

Anyhow, the WikiOutput-derived classes, HTMLWikiOutput and TestWikiOuput, need their constructors changed, too:

public HTMLWikiOutput(WikiOutput parent, LinkMaker lm) : base(parent, lm)
{
}

... and ...

public TestWikiOutput(WikiOutput parent, LinkMaker lm) : base(parent, lm)
{
}

Finally, each of the Formatter calls to WikiOutput.ForFormat need to be modified to account for the new LinkMaker parameter. The calls appear in the following Formatter methods:

  • FormattedString
  • FormattedTopicWithSpecificDiffs
  • TranslateBehaviorsAndEverythingElse
  • ErrorMessage
  • IncludedTopic
  • NestedFormat

For the first two, use the lm parameter passed to the method, and for all the others use the Formatter's LinkMaker method.

There's also an unfortunate direct instantiation of an HTMLWikiOutput object in the Federation's GetTopicFormattedBorder method (so much for output-independence). Pass it the Federation's LinkMaker property.

Finally, we can write the HTMLWikiOutput.WriteScripts method like this:

void WriteScripts()
{
  Write(@"<script type=""text/javascript"" src=""" + LinkMaker.SiteURL() + "HTMLWikiOutput.js" + @"""></script>");
}

Next up, fixing the JavaScript, which we pulled from WriteScripts, to work in FireFox.

FlexWiki Notes - Indented Paragraphs
FlexWiki Notes - Starting Ordered Lists
FlexWiki Notes - Multiple Namespaces
FlexWiki Notes - Getting Started
FlexWiki Notes


Comments


TrackBack

TrackBack URL:  http://www.typepad.com/services/trackback/6a00d8341c7bd453ef00d83551b28369e2

Listed below are links to weblogs that reference FlexWiki Notes - JavaScript Issues, Part 1: