Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Building an XSL Transform Tool

  Purpose of this tool is to apply the transformation in XSL on XML file and display the results.This tool is basically a Simple WinForm where the location of an XML file and a corresponding XSL file are specified. It then displays the results of the transformation in text box for review.

Difficulty: Intermediate
Time Required: 1-3 hours
Cost: Free
Software: Visual Studio Express Editions
Hardware:
Download:

A while back I got tired of looking for a simple solution to performing 'on the fly' XSL Transformations. I didn't want some memory-hog tool to completely isolate me from the details of the transform through layers of abstraction—just a light-weight tool that would act as a simple transformation engine. I just needed something to run a transform based on my stylesheet and let me quickly check the results in an iterative fashion. I looked for a while, and didn't find anything like that available. So I built my own simple solution. It wasn't much—just a simple WinForm where I could specify the location of an XML file and a corresponding XSL file and then push a button to fire the results of the transformation to a text box for review. While that worked fine, I recently decided that my home-grown solution needed a bit of a face-lift. Nothing dramatic. Just a few tweaks to make it easier to fine-tune my stylesheets on the fly.

A Glorified Text Editor

To keep things simple, I just wanted the added ability to edit my XML and XSL (mostly the latter) documents in the same tool that I used to transform them. I'd been using either an open instance of Visual Studio to edit them (nice, but heavy), or NotePad. In a perfect world, I'd get some of the key benefits of an editor like Visual Studio, but with the "weight" of something like NotePad. NotePad is actually fine for editing text files—only I hate the fact that it doesn't remember my tab position. In other words, if I've "tabbed over" three times (for formatting purposes) and type something followed by a carriage return, I'm back at the left margin, and not tabbed to the place where I was in the previous line. So, my thoughts on the upgrade were that if I could just get the text boxes where I would be doing my editing to remember my tab position, then I'd be happy. Only... I really like having tag completion in Visual Studio as well. It's not like I can't do it myself, but I certainly like having my tags closed automatically. So, I figured I needed both aspects in my new "editor" enabled version of my transform tool to make me happy.

Making Mikey Happy: Remembering Tab Position

Figuring out how to remember tab positioning turned out to be tougher than I imagined—but in a strange way. Coding the solution wasn't very hard at all, and only took about half an hour to perfect. The hard part was figuring out that I had to code it myself. For some reason I figured that something in the Framework would make the task almost as easy as tweaking a property on one of the built-in TextBox controls that ship with the Framework. After poring over the MSDN docs, trying to track down existing solutions on the web, and bugging a listserve that I spend a lot of time on, I decided to just write the functionality myself.

Once I made the decision to code the functionality myself, the task became fairly easy. The trick was just to figure out what the number of preceding tabs were in the previous line, and plunk the same number of tabs in front of the cursor each time that a carriage return was detected. The logic to handle all of this was pretty simple and just revolved around getting a handle on the textbox housing the desired text, and then calculating the preceding line and existing tab count. Once those were located, all I had to do was insert a corresponding number of tabs into the textbox at the current position, and then advance the cursor by moving the SelectionStart property:

Visual C#

private bool HandleBlockTabbing(object sender)
{
TextBox current = (TextBox)sender;
int charIndex = current.SelectionStart;
string work = current.Text.Substring(0, charIndex);

int currentLineNumber = current.GetLineFromCharIndex(charIndex);

// the carriage return hasn't happened yet...
// so the 'previous' line is the current one.
string previousLineText;
if(current.Lines.Length <= currentLineNumber)
previousLineText = current.Lines[current.Lines.Length - 1];
else
previousLineText = current.Lines[currentLineNumber];

if (previousLineText.StartsWith("\t"))
{
string tabs = string.Empty;
// first non-TAB character
Match m = Regex.Match(previousLineText, "[^\t]");
if (m.Success)
tabs = previousLineText.Substring(0,
previousLineText.IndexOf(m.Value));
else // there were only tabs (no other chars)
tabs = previousLineText;

string added = "\r\n" + tabs;

current.Text = current.Text.Insert(charIndex, added);
current.SelectionStart = charIndex + added.Length;

return true;
}

return false;
}

Visual Basic

Private Function HandleBlockTabbing(ByVal sender As Object) As Boolean
Dim current As SimpleEditBox = CType(sender, SimpleEditBox)
Dim charIndex As Integer = current.SelectionStart
Dim work As String = current.Text.Substring(0, charIndex)
Dim currentLineNumber As Integer =
current.GetLineFromCharIndex(charIndex)

' the carriage return hasn't happened yet...
' so the 'previous' line is the current one.
Dim previousLineText As String

If current.Lines.Length <= currentLineNumber Then
previousLineText = current.Lines(current.Lines.Length - 1)
Else
previousLineText = current.Lines(currentLineNumber)
End If

If previousLineText.StartsWith("" & vbTab) Then
Dim tabs As String = String.Empty
' first non-TAB character
Dim m As Match = Regex.Match(previousLineText, "[^\t]")
If m.Success Then
tabs = previousLineText.Substring(0,
previousLineText.IndexOf(m.Value))
Else
tabs = previousLineText
End If
Dim added As String = (vbCrLf + tabs)
current.Text = current.Text.Insert(charIndex, added)
current.SelectionStart = (charIndex + added.Length)
Return True
End If
Return False
End Function

One big thing to note about the above code is that it's taking place in the KeyPress event—so the carriage return hasn't really "happened" yet; it's currently in the state of being processed. But once the KeyPress event is handled for a carriage return, the logic above makes a screen shot like the one below possible:

The cool thing is that in the above screen shot, the logic discerned that the previous line was one tab over and then just inserted a carriage return plus a single tab into the current line of text (technically the third line of text, because the carriage return hasn't actually happened yet). Once that was put in place, the current selection was advanced to the end of the carriage return and the newly inserted tab. Then the textbox was "told" that the keystroke was handled (the return true part of the code—more on that in a second), and the textbox therefore doesn't need to do any more processing on the keystroke (like add a carriage return of its own). The beauty of this approach is that there are no spooky characters in place; a simple BACKSPACE will remove the existing tab. It's like we've just used the textbox to do our typing for us, that's all.

Making Mikey Happy: Tag Completion

I initially figured that tag completion would just be a question of using a regular expression to determine the name of the currently uncompleted tag (if there was one) each time a ">" was detected, and then adding closing tags as needed. It turned out to be about that hard, only I needed to account for directives, comments, and empty tags as well. I also needed to be able to account for stray ">" characters. The goal of my tag completion was to create functionality that was more like cruise control (a lazy person's driving aid), instead of an auto-pilot (where you'd effectively give up control of what was going on). To handle all of these contingencies, I just threw in a helper method where I could encapsulate all of the various permutations and just report to the caller (main logic) if it was to close the tag or not. Here's the code to handle the detection of a ">" in the KeyPress event:

Visual C#

private bool HandleBlockTabbing(object sender)
{
TextBox current = (TextBox)sender;
int charIndex = current.SelectionStart;
string work = current.Text.Substring(0, charIndex);

int currentLineNumber = current.GetLineFromCharIndex(charIndex);

// the carriage return hasn't happened yet...
// so the 'previous' line is the current one.
string previousLineText;
if(current.Lines.Length <= currentLineNumber)
previousLineText = current.Lines[current.Lines.Length - 1];
else
previousLineText = current.Lines[currentLineNumber];

if (previousLineText.StartsWith("\t"))
{
string tabs = string.Empty;
// first non-TAB character
Match m = Regex.Match(previousLineText, "[^\t]");
if (m.Success)
tabs = previousLineText.Substring(0,
previousLineText.IndexOf(m.Value));
else // there were only tabs (no other chars)
tabs = previousLineText;

string added = "\r\n" + tabs;

current.Text = current.Text.Insert(charIndex, added);
current.SelectionStart = charIndex + added.Length;

return true;
}

return false;
}

Visual Basic

Private Function HandleBlockTabbing(ByVal sender As Object) As Boolean
Dim current As SimpleEditBox = CType(sender, SimpleEditBox)
Dim charIndex As Integer = current.SelectionStart
Dim work As String = current.Text.Substring(0, charIndex)
Dim currentLineNumber As Integer =
current.GetLineFromCharIndex(charIndex)

' the carriage return hasn't happened yet...
' so the 'previous' line is the current one.
Dim previousLineText As String

If current.Lines.Length <= currentLineNumber Then
previousLineText = current.Lines(current.Lines.Length - 1)
Else
previousLineText = current.Lines(currentLineNumber)
End If

If previousLineText.StartsWith("" & vbTab) Then
Dim tabs As String = String.Empty
' first non-TAB character
Dim m As Match = Regex.Match(previousLineText, "[^\t]")
If m.Success Then
tabs = previousLineText.Substring(0,
previousLineText.IndexOf(m.Value))
Else
tabs = previousLineText
End If
Dim added As String = (vbCrLf + tabs)
current.Text = current.Text.Insert(charIndex, added)
current.SelectionStart = (charIndex + added.Length)
Return True
End If
Return False
End Function

Note that, in many ways, it's very similar to the code that handles tabbing: it just determines if characters need to be added, and then inserts them if needed. The difference is that it only advances the cursor one space when it's done doing the inserting task, so as to place the cursor between the beginning and ending tags. Whether or not the tag needs to be closed is decided by the following bit of helper logic:

Visual C#

private bool CurrentPositionNeedsClosingTag(string input)
{
string scrubbedText = input.Replace("\r", string.Empty);
scrubbedText = scrubbedText.Replace("\n", string.Empty);
scrubbedText = scrubbedText.Replace(" ", string.Empty);
// check lots of various tag ending types, and then other issues/hacks

// ignore directives, comments, and empty tags:
bool isDirective = scrubbedText.EndsWith("?");
bool isComment = scrubbedText.EndsWith("--");
bool isEmptyTag = scrubbedText.EndsWith("/");
if (isDirective || isComment || isEmptyTag)
return false;

// watch out for stray tags and truly empty tags ("<>")
if (scrubbedText.EndsWith(">") || scrubbedText.EndsWith("<>"))
return false;

// watch out for explicit closing tags
// ("</endMyTagByHand>" - don't want: "</end><//end>")
// HACK: should be done with a regex..
int position = scrubbedText.LastIndexOf("<");
if (scrubbedText.Substring(position).StartsWith("</"))
return false;

return true;
}

Visual Basic

Private Function CurrentPositionNeedsClosingTag(ByVal input As String) As Boolean
Dim scrubbedText As String = input.Replace(vbCr, String.Empty)
scrubbedText = scrubbedText.Replace(vbLf, String.Empty)
scrubbedText = scrubbedText.Replace(" ", String.Empty)
' check lots of various tag ending types, and then other issues/hacks
' ignore directives, comments, and empty tags:
Dim isDirective As Boolean = scrubbedText.EndsWith("?")
Dim isComment As Boolean = scrubbedText.EndsWith("--")
Dim isEmptyTag As Boolean = scrubbedText.EndsWith("/")
If (isDirective _
OrElse (isComment OrElse isEmptyTag)) Then
Return False
End If
' watch out for stray tags and truly empty tags ("<>")
If (scrubbedText.EndsWith(">") OrElse scrubbedText.EndsWith("<>"))
Then
Return False
End If
' watch out for explicit closing tags
' ("</endMyTagByHand>" - don't want: "</end><//end>")
' HACK: should be done with a regex..
Dim position As Integer = scrubbedText.LastIndexOf("<")
If scrubbedText.Substring(position).StartsWith("</") Then
Return False
End If
Return True
End Function

Once in place, the tag completion turned out to be pretty spiffy. It won't close comments, directives, or empty tags (i.e. <tag />); just open tags, even ones that span multiple lines:

Coupled with the "tab memory" functionality, I now had enough editor functionality to make me happy.

Making Consumers Happy: Subclassing

When I was first testing out the tabbing and tag-completion logic, I just did it in a WinForm, and handled the KeyPress event. My long-term goal, though, was to create a subclass of System.Windows.Forms.TextBox that would encapsulate this logic away to make reuse much easier. Imagine trying to handle tabbing and tag-completion for three textboxes on the same form, for example; it could be done, but wouldn't be pretty. Creating the subclass of TextBox was very easy. I just added a new user control to my current project, changed its inheritance from UserControl to TextBox and then dropped in the code as follows:

Visual C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Text.RegularExpressions;

namespace XSLT_Former
{

public partial class SimpleEditorBox : TextBox
{
public SimpleEditorBox()
{
InitializeComponent();

// make sure that tab and enter are allowed by
// default in this specialized text box:
base.AcceptsReturn = true;
base.AcceptsTab = true;
}

protected override void OnKeyPress(KeyPressEventArgs e)
{
if (e.KeyChar == 13) // enter/carriage return
e.Handled = this.HandleBlockTabbing(this);
if (e.KeyChar == '>')
e.Handled = this.HandleTagCompletion(this);

base.OnKeyPress(e);
}
private bool HandleBlockTabbing(object sender)
{
// elided - code same as above
}

private bool HandleTagCompletion(object sender)
{
// elided - code same as above
}

private bool CurrentPositionNeedsClosingTag(string input)
{
// elided - code same as above
}
}
}

Visual Basic

Imports System.Text.RegularExpressions

Public Class SimpleEditBox
Inherits System.Windows.Forms.TextBox

Public Sub New()
' This call is required by the Windows Form Designer.
InitializeComponent()

MyBase.AcceptsReturn = True
MyBase.AcceptsTab = True
End Sub

Protected Overrides Sub OnKeyPress(ByVal e As KeyPressEventArgs)
If (e.KeyChar = CType(ChrW(13), Char)) Then
e.Handled = Me.HandleBlockTabbing(Me)
End If
If (e.KeyChar = CType(ChrW(62), Char)) Then
e.Handled = Me.HandleTagCompletion(Me)
End If
MyBase.OnKeyPress(e)
End Sub

Private Function HandleBlockTabbing(ByVal sender As Object) As Boolean
' elided - code same as above
End Function

Private Function HandleTagCompletion(ByVal sender As Object)
As Boolean
' elided - code same as above
End Function

Private Function CurrentPositionNeedsClosingTag(ByVal input As String)
As Boolean
' elided - code same as above
End Function
End Class

As you can see, nothing special. The KeyPress event is no longer being handled; rather, the OnKeyPress method has been safely overridden such that this control can be further sub classed if desired. The big plus, of course, is that dropping a SimpleEditBox onto a WinForm provides a textbox that now remembers tab position and closes XML (and HTML) tags.

Creating a Work Area

With a SimpleEditorBox control now available to handle the whiz-bang features for text editing, it was time to create a WinForm that I could use as a work area. In it I needed an area to display the XML document, an XSL document (or my stylesheet) as well as the output. I wanted all three areas to editable, but maximizing the space for each area would have been problematic to try and manage. So I cheated and just used two splitter controls to allow a high degree of flexibility at run time. The first splitter divided the top (work area) from the bottom (output/results area) so that either section could be sized as needed. The second splitter—placed in the top half of the previous splitter—was to divide the XML work area from the XSL work area as follows:

Each work area was then fitted with a few simple controls to allow for the loading and saving of files. A button to execute the transform was placed in the bottom of the form, which only becomes enabled once both work areas have a file loaded. By playing around with the Anchor properties for each control and GroupBox, each work area not only retains a decent perspective when the Winform is maximized, but as the sliders are moved around to focus on one are or the other, the other two areas maintain a decent aspect ratio as well (even when seriously "scrunched" so as to appear inside an image like the one below):

As seen in the above image, I'm focusing my attention on the transform, where I've dedicated more of my real estate by tugging on the splitter bars to the desired sizing. However, the output and XML areas are still coherent and could easily become the focus of my efforts just through some more dragging on the darkened splitter bars.

Doing the Transform

The logic to handle the transform itself ends up being some pretty simple code. All it needs to be able to do is stream the contents of the XML and XSL SimpleEditBoxes in as XML, and run them through a transform.

Visual C#

private void TransformOutput()
{
string xml = this.edtXml.Text.Trim();
string xsl = this.edtXsl.Text.Trim();

try
{
// stream the xml:
byte[] data = Encoding.UTF8.GetBytes(xml);
MemoryStream ms = new MemoryStream(data);

XmlReader input = XmlReader.Create(ms); // to allow modification by xform

// load the xsl:
XmlDocument stylesheet = new XmlDocument();
stylesheet.LoadXml(xsl);

XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(stylesheet);

// create output streams:
MemoryStream buffer = new MemoryStream();
StreamWriter sw = new StreamWriter(buffer);

// transform the document:
transform.Transform(input, null, sw);

// turn results into a string:
byte[] chars = buffer.ToArray();
string output = Encoding.UTF8.GetString(chars);

this.edtOut.Text = output;

this.btnSaveOutput.Enabled = true;
}
catch (Exception ex)
{
this.edtOut.Text = "Error Occurred: " + Environment.NewLine + ex.Message;
}
}

Visual Basic

Private Sub TransformOutput()
Dim xml As String = Me.edtXml.Text.Trim
Dim xsl As String = Me.edtXsl.Text.Trim
Try
' stream the xml:
Dim data() As Byte = Encoding.UTF8.GetBytes(xml)
Dim ms As MemoryStream = New MemoryStream(data)
Dim input As XmlReader = XmlReader.Create(ms)
' to allow modification by xform
' load the xsl:
Dim stylesheet As XmlDocument = New XmlDocument
stylesheet.LoadXml(xsl)
Dim transform As XslCompiledTransform = New XslCompiledTransform
transform.Load(stylesheet)
' create output streams:
Dim buffer As MemoryStream = New MemoryStream
Dim sw As StreamWriter = New StreamWriter(buffer)
' transform the document:
transform.Transform(input, Nothing, sw)
' turn results into a string:
Dim chars() As Byte = buffer.ToArray
Dim output As String = Encoding.UTF8.GetString(chars)
Me.edtOut.Text = output
Me.btnSaveOutput.Enabled = True
Catch ex As Exception
Me.edtOut.Text = ("Error Occurred: " _
+ (Environment.NewLine + ex.Message))
End Try
End Sub

Exceptions are handled in a very simple fashion by just outputting the details of the exception to the output window. If no exception occurs, then the transform results are written to the output window. There they can be modified as needed or saved to disk using the menu options or button control available.

And there it is: a simple tool that allows you to get your hands dirty with the gory details of XSLT, along with a simple button that will render the transform for you whenever you are ready. The simple additions to the editor panes make it easier to perform ad hoc modification of the XML and XSL documents, and being able to render the transform on the fly helps make for a great way to modify your stylesheet in an iterative fashion.

Follow the Discussion

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.