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.
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())
{
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())
{
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.
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 =
{
new RegexCompilationInfo
(
KnownXslTagsPattern
, RegexOptions.None
, "KnownXslTagsRegex"
, NAMESPACE, true
)
};
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = NAMESPACE;
assemblyName.Version = new Version("1.0.0.0");
Regex.CompileToAssembly(compInfo, assemblyName);
}
Syntax highlighting can be adjusted using the SyntaxSettings
property of XSLRichTextBox
. Syntax highlighting notes:
- 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.
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.
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:
- I did not finish it,
- it's not so intelligent, and
- 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)
{
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
{
return false;
}
}
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;
int lastVisibleLineStart = _xslRichTextBox.GetFirstCharIndexFromLine(
_xslRichTextBox.LastVisibleLine);
Point lastVisibleLineStartPos = _xslRichTextBox.GetPositionFromCharIndex(
lastVisibleLineStart);
if (intellisensePos.Y +
_intellisenseListBox.Height > lastVisibleLineStartPos.Y)
{
intellisensePos.Y -=
_intellisenseListBox.Height + MARGIN + _xslRichTextBox.Font.Height;
}
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