Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

XSL Editor Control

0.00/5 (No votes)
13 Jun 2009 2  
A basic WinForms XSL editor control
XSLEditorDemoScreenShot.jpg

Introduction

This is an example of a basic WinForms XSL editor control with colorized syntax highlighting and XSL transformation functions.

Background

Three years ago, I started an addin at my company to do various Oracle and WinForms code generation and related tasks (perhaps article(s) on that later). The code generation templates started off as simple code blocks with string tokens replaced at runtime. As the template tasks became more involved, I realized I was starting to create my own scripting language and parser. At that point I transitioned the data to XML, language to XSL and used XSLT to do the code generation transformations (Oracle XML schema data to SQL in this case).

I wanted to be able to edit the templates within an addin form, but did not want to edit large portions of XSL in a plain textbox. I really wanted a free XSL editor control but could not easily find one at that time. As such I decided to create a basic XSL editor control similar to the one in this article.

Disclaimer and Limitations

I will go ahead and state a few points upfront to set the scene:

  • A full-blown XSL editor was not the goal with my tool - only means to end of saving developer time.
  • The primary intent was to provide XSL syntax highlighting and transformation functions for codegen (anything else a bonus).
  • I knew I would not be able to complete some features such as intellisense, XSLT debugging.
  • Elegant code was a luxury I did not have time for; this tool was for codegen and knew it would not be maintained much.

Using the Code

The class diagram below shows the main classes of interest. The two main starting classes are:

  • XSLRichTextBox - The core custom RichTextBox control with XSL-specific syntax highlighting, transformation methods, and partial intellisense support.
  • XSLEditor - A wrapper user control that contains an XSLRichTextBox, status bar, XML syntax validation pane, and a toolbar that invokes common editing operations.

The XSLRichTextBox class gets used either way. Whether you use the XSLEditor control depends on whether its 'canned' UI and functionality suits your app. Using only XSLRichTextBox can require more code but is more lightweight and flexible.

All the main classes are in the XSL.Library project. SyntaxRichTextBox resides in the Common.Library project.

Main Classes

Syntax Highlighting

XSLRichTextBox is derived from a modified version of the SyntaxRichTextBox base class from Patrik Svensson's SyntaxRichTextBox article on CodeProject. SyntaxRichTextBox provided a good line-by-line RegEx-based text colorizer base to start with. My version of SyntaxRichTextBox contains additional non-XSL specific editor functionality (line & column info, dirty state, printing code...) and some changes to enhance performance and provide overridable syntax highlighting. I overrode a ProcessLine() method in XSLRichTextBox, calling ProcessXmlTags() to handle the more complex XSL tags with multiple sub-parts, for each of which I wanted a different color.

protected void ProcessXmlTags()
{
	SuspendOnTextChanged();
	int nStart, nLength;
	KnownXslTagsRegex tagRegEx = new KnownXslTagsRegex();

	for (Match regMatch = tagRegEx.Match(m_strLine); 
		regMatch.Success; regMatch = regMatch.NextMatch())
	{               
		// group 0: full tag.   group 1: :tagname w/o namespace prefix
		// group 2: all attributes.   group 3: attrname="attrvalue" (last set)
		// group 4: ="attrvalue" (last set).   group 5: ?
		// group 6: tag name.    group 7: attribute name (last set)
		// group 8: attribute value (last set).   group 9: / closing tag

		string fullTag = regMatch.Groups[0].Value;
		string tagName = regMatch.Groups[1].Value;
		nStart = m_nLineStart + regMatch.Index + 1;
		if (fullTag.StartsWith("</")) nStart++;
		
		nLength = tagName.Length;
		SelectionStart = nStart;
		SelectionLength = nLength;
		SelectionColor = SyntaxSettings.XslTagColor;

		AttributeNameValueRegex attrRegEx = new AttributeNameValueRegex();
		string attrs = regMatch.Groups[2].Value;
		for (Match attrMatch = attrRegEx.Match(attrs); 
			attrMatch.Success; attrMatch = attrMatch.NextMatch())
		{
			//group[0] = all (attr=value), 
			//group[1] = attrname, group[2] = attrvalue
			string attrName = attrMatch.Groups[1].Value;
			string attrValue = attrMatch.Groups[2].Value;

			SelectionStart = nStart + tagName.Length + attrMatch.Index;
			SelectionLength = attrName.Length;
			SelectionColor = SyntaxSettings.AttributeNameColor;

			SelectionStart += attrName.Length + 2;
			SelectionLength = attrValue.Length;
			SelectionColor = SyntaxSettings.AttributeValueColor;

			KnownXslFunctionsRegex funcRegEx = 
					new KnownXslFunctionsRegex();
			for (Match funcMatch = funcRegEx.Match(attrValue); 
			    funcMatch.Success; funcMatch = funcMatch.NextMatch())
			{
				string func = funcMatch.Value;
				SelectionStart = nStart + tagName.Length + 
					attrMatch.Index 
					+ attrName.Length + 2 + funcMatch.Index;
				SelectionLength = func.Length;
				SelectionColor = SyntaxSettings.XslFunctionsColor;

				int pos = this.Text.IndexOf(")", 
						SelectionStart + 1);
				SelectionStart = pos;
				SelectionLength = 1;
				SelectionColor = SyntaxSettings.XslFunctionsColor;
			}
		}
	}

	ResumeOnTextChanged();
}

The RegEx classes used in the syntax highlighting are defined in the RegularlyExpressYourself.cs file :) and are compiled into XSL.Library.RegularExpressions.dll for performance. I just call the function GenerateRegExAssembly() once from the VS.NET debugger if any changes are made to the regular expressions. The main pattern is for the XSL tags which is a slightly modified XML tag pattern with a list of all the valid XSL tags. This was done so if there were any tag typos or plain XML tags, they would not be colorized to indicate a likely problem.

/// <summary>
/// Matches all currently known xsl tags (complete list truncated for brevity)
/// </summary>
public static string KnownXslTagsPattern
{
	get
	{
		string pattern = 
		"<(?<endTag>/)?(?<tagname>(xsl:apply-imports|xsl:apply-templates)?)
		((\\s+(?<attName>\\w+(:\\w+)?)" 
		+ "(\\s*=\\s*(?:\"(?<attVal>[^\"]*)\"|'(?<attVal>[^']*)'|
		(?<attVal>[^'\">\\s]+)))?)+\\s*|\\s*)(?<completeTag>/)?>";
		return pattern;
	}
}

public static void GenerateRegExAssembly()
{
	const string NAMESPACE = "XSL.Library.RegularExpressions";
	RegexCompilationInfo[] compInfo = 
	{
		// Matches known XSL tags
		new RegexCompilationInfo
		(
			KnownXslTagsPattern
			, RegexOptions.None
			, "KnownXslTagsRegex"
			, NAMESPACE, true
		)  // remaining omitted...
	};
	AssemblyName assemblyName = new AssemblyName();
	assemblyName.Name = NAMESPACE;
	assemblyName.Version = new Version("1.0.0.0");
	Regex.CompileToAssembly(compInfo, assemblyName); // vs.net dir \ common \ ide
}	

Syntax highlighting can be adjusted using the SyntaxSettings property of XSLRichTextBox. Syntax highlighting notes:

Syntax Settings

  • Some of the SyntaxSettings are not currently used (i.e. Comment pattern, Integers, Strings)
  • Tags are colorized one line at a time as you type
  • Setting the Text property results in colorizing all tags/lines
  • Pasting existing colorized text works but pasting plain text tags currently only colorizes the last line
  • Coloring isn't as fast as say VS.NET but decent for my use with turning redraw off, lock window update, etc. The demo XSL is ~700 lines and ~28,000 characters and is colorized in around 2 or 3 seconds (Vista 3.16 Ghz, 4 GB RAM)

Transformations

XSLT functions can be invoked through several Transform() method overloads in 3 locations: XSLRichTextBox (most ideal area), XSLEditor control, or directly from TransformUtility static methods.

Generally you would invoke a XSLRichTextBox.Transform() method, pass in the XML (XMLDocument, FileInfo, or XML string) and optionally any TransformSettings (depending on whether defaults suit needs). Transform() hands back a TransformResults object and it is up to you what to do with that output data.

XSL Parameters

One interesting point is the optional but default dynamic parameter prompting. I wanted a mechanism to pass parameter values into the various xsl:param tags, without requiring writing code or parameter forms, keeping everything generic.

Dynamic parameter prompting

By default, each parameter is pulled into the dynamic form, with the param name being the label, and the value input being a TextBox. The form sizes according to the number of parameters or does not show at all if there are no parameters or if EnableParamPrompting is false. In order to customize the parameter prompting UI, I had to add some of my own custom* attributes to the xsl:param tags in order to give the UI hints about the control type, default data, etc. Support was also added for some .NET method invocations such as Environment.MachineName, DateTime.Now, etc. You could argue this "pollutes" the XSL with custom syntax for transformation UI metadata but this was exactly what I needed. Again the parameter prompting can be turned off and any values passed in manually.

 <xsl:param name="PACKAGE_NAME" select="concat
	('PKG_', OracleTableInfo/Schema/Tables/Table[1]/@ID)"/> 
 <xsl:param name="AUTHOR_NAME" value="{Environment.UserName}" />
 <xsl:param name="CREATED_DATE" 
	value="{DateTime.Now.ToShortDateString()}" customReadOnly="true" />
 
 <xsl:param name="CHILD_TABLES" customControlType="CheckedListBox" 
	customListXPath="//ChildTables/Table/@ID" customCheck="All"/>
 <xsl:param name="PARENT_TABLES" customControlType="CheckedListBox" 
	customListXPath="//ParentTables/Table/@ID" customCheck="All"/>

 <xsl:param name="_VIEW_NAME" value="" customHidden="true" />
 <xsl:param name="_USE_VIEW_FOR_SEARCH" value="False" customHidden="true" />

The Default Transformation Form

If you call XSLRichTextBox.Transform() with no parameters, or if you invoke default transformation using the XSLEditor control (i.e. via Toolbar), you will get a default dialog allowing you to input an XML filename, specify settings, transform and view results. If you are using the XSLEditor control and do not want to use this dialog, you could wire into the ActionBeforeExecute event, check the action type, perform your own functionality and set e.Cancel to cancel the default functionality.

Default transform dialog

Sample Data

DEMO_ORDERS.xml was produced by my addin tool against a sample Oracle XE database and is included with the demo solution. The demo will load with sample XSL (written in the editor naturally) to generate Oracle package body SQL when the transformation is run. The addin version of the SQL output uses an Oracle SQL RichTextBox (a la unoffical Oracle Developer Tools use) but that's another story.

Intellisense

I almost hesitate to even mention "Intellisense" as:

  1. I did not finish it,
  2. it's not so intelligent, and
  3. the implementation is not what you would want to build on.

In fact Intellisense is turned off by default. All I really cared about was getting a list of tags to popup with a tooltip description of each. I was looking for built-in reference more so than inserting/editing capabilities. The basic tags are insertable but I pretty much stopped there. Some attributes show up for some tags and XSL functions are loaded but do not appear in Intellisense.

The IntellisenseMgr class gets instantiated from the XSLRichTextBox constructor. IntellisenseMgr then wires into change events of the XSLRichTextBox, and if Intellisense is enabled, various setup functions are called if the appropriate keystrokes are sent.

Some initial one-time setup involves building intellisense data (done via very simple, boring POCO classes), wiring events and setting up the intellisense listbox:

private void AddXSLTags()
{
	_xslTags = XSLTags.GetXSLTags();
}

private void AddXSLFunctions()
{
	_xslFunctions = XSLFunctions.GetXslFunctions();
}

private void InitializeIntellisense()
{
	AddXSLTags();
	AddXSLFunctions();

	_intellisenseListBox = new ListBox();
	_intellisenseListBox.Name = "_intellisenseListBox";
	_intellisenseListBox.Size = new Size(250, 100);
	_intellisenseListBox.Visible = false;
	_intellisenseListBox.DataSource = _xslTags;
	_intellisenseListBox.DisplayMember = "TagName";
	_intellisenseListBox.Leave += new EventHandler(_intellisenseListBox_Leave);
	_intellisenseListBox.KeyDown += 
		new KeyEventHandler(_intellisenseListBox_KeyDown);
	_intellisenseListBox.DoubleClick += 
		new EventHandler(_intellisenseListBox_DoubleClick);
	_intellisenseListBox.SelectedValueChanged += new EventHandler(
		_intellisenseListBox_SelectedValueChanged);
	_intellisenseTooltip = new ToolTip();
	_intellisenseListBox.Cursor = Cursors.Arrow;
	_intellisenseListBox.Sorted = true;
	
	_xslRichTextBox.Controls.Add(_intellisenseListBox);
}

Some per-invocation setup:

private bool PrepareIntellisense(string charPressed)
{
	//ensure one-time, initial setup has been performed
	if (!IntellisenseInitialized) InitializeIntellisense();

	this.IsContextInfoCurrent = false;

	if (" " == charPressed)
	{
		XSLTag curTag = this.CurrentTag;

		if (null != curTag)
		{                    
			if (!InAttributeValue)
			{
				_intellisenseListBox.DataSource = curTag.Attributes;
				_intellisenseListBox.DisplayMember = "Name";
				_intellisenseListBox.SelectedIndex = -1;
			}
			else
			{
				//TODO: functions(), variable list etc.
				return false; // for now until ready
			}
		}
		else
		{
			return false;
		}
	}
	else if ("<" == charPressed)
	{
		_intellisenseListBox.DataSource = _xslTags;
		_intellisenseListBox.DisplayMember = "TagName";
	}

	this.IsContextInfoCurrent = true;
	return true;
}

private void ShowIntellisense(string charPressed)
{
	const int MARGIN = 5;
	if (!PrepareIntellisense(charPressed)) return;

	Point intellisensePos = _xslRichTextBox.GetPositionFromCharIndex(
		_xslRichTextBox.SelectionStart);
	intellisensePos.Y = intellisensePos.Y + _xslRichTextBox.Font.Height + MARGIN;

	//calculate whether proposed intellisense position will be 
	//off-screen beneath the last visible line
	int lastVisibleLineStart = _xslRichTextBox.GetFirstCharIndexFromLine(
		_xslRichTextBox.LastVisibleLine);
	Point lastVisibleLineStartPos = _xslRichTextBox.GetPositionFromCharIndex(
		lastVisibleLineStart);

	if (intellisensePos.Y + 
		_intellisenseListBox.Height > lastVisibleLineStartPos.Y)
	{
		// just show intellisense on top of text if at bottom
		// (scrolling down then showing beneath does not appear to work)
		intellisensePos.Y -= 
		_intellisenseListBox.Height + MARGIN + _xslRichTextBox.Font.Height;
	}

	// see if intellisense x pos will put it off screen to the right
	if (intellisensePos.X + _intellisenseListBox.Width > _xslRichTextBox.Width)
	{
		int diff = (intellisensePos.X + 
			_intellisenseListBox.Width) - _xslRichTextBox.Width;
		intellisensePos.X -= diff + 25;
	}

	_intellisenseListBox.Location = intellisensePos;
	_intellisenseListBox.Visible = true;
	_intellisenseListBox.Focus();
}

Other Features

  • Validation - via XMLEditor control; this is automatically validating the XML really, not XSL. Running the transformation seemed the only complete validation and that was too expensive to be done so frequently. There is an xslt.xsd in Program Files\Microsoft Visual Studio 9.0\Xml\Schemas that I thought about taking advantage of at some point.
  • Find / Replace & common editing - via XMLEditor control and XSLRichTextBox: standard simple find/replace dialog, open, save, print, etc.

Points of Interest

It is interesting how scope creep can get the best of us. Codesmith turned into an addin and a template turned into an XSL editor project that was perhaps more work than the rest of the main functionality. I'm sure I missed easier ways to accomplish some of this as well but it was a fun learning experience. It has also made me more appreciative of the power and beauty of rich code editors like VS.NET.

Well this was my first CodeProject article so go easy on me. :) I tried to cleanly strip out most of the domain-specific stuff out of the source addin project into the demo solutions. I had to do that pretty quickly though so I'm sure some things have been missed and much of it not thoroughly tested.

History

  • 06/08/2009 - Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here