XSL 编辑器控件






4.84/5 (14投票s)
一个基本的 WinForms XSL 编辑器控件

引言
这是一个基本的 WinForms XSL 编辑器控件的示例,具有着色语法高亮和 XSL 转换功能。
背景
三年前,我在公司开始了一个插件,用于执行各种 Oracle 和 WinForms 代码生成及相关任务(也许稍后会写关于这些的文章)。代码生成模板最初只是简单的代码块,在运行时替换字符串标记。随着模板任务变得越来越复杂,我意识到我正在创建自己的脚本语言和解析器。那时,我将数据迁移到了 XML,语言切换到 XSL,并使用 XSLT 进行代码生成转换(在这种情况下,是从 Oracle XML 架构数据到 SQL)。
我希望能够在插件窗体中编辑模板,但不想在纯文本框中编辑大段的 XSL。当时我真的很想要一个免费的 XSL 编辑器控件,但找不到。因此,我决定创建一个基本的 XSL 编辑器控件,类似于本文中的控件。
免责声明和限制
我将首先陈述几点,以设定背景
- 我开发的工具的目标并不是一个功能齐全的 XSL 编辑器,仅仅是为了节省开发人员的时间。
- 主要目的是为代码生成提供 XSL 语法高亮和转换功能(其他任何功能都是附加的)。
- 我知道我无法完成某些功能,例如智能感知、XSLT 调试。
- 优美的代码对我来说是奢侈品,我没有时间实现;这个工具是为了代码生成,我知道它不会被过多维护。
Using the Code
下面的类图显示了主要相关的类。两个主要的起始类是
XSLRichTextBox
- 核心自定义RichTextBox
控件,具有 XSL 特定语法高亮、转换方法和部分智能感知支持。XSLEditor
- 一个包装用户控件,包含一个XSLRichTextBox
、状态栏、XML 语法验证窗格和一个调用常见编辑操作的工具栏。
XSLRichTextBox
类无论如何都会被使用。是否使用 XSLEditor
控件取决于其“固定”的用户界面和功能是否适合您的应用程序。只使用 XSLRichTextBox
可能需要更多代码,但更轻量级且更灵活。
所有主要类都在 _XSL.Library_ 项目中。SyntaxRichTextBox
位于 _Common.Library_ 项目中。

语法高亮
XSLRichTextBox
是从 CodeProject 上 Patrik Svensson 的 SyntaxRichTextBox
文章中的 SyntaxRichTextBox
基类的一个修改版本派生而来的。SyntaxRichTextBox
提供了一个良好的基于 RegEx
的逐行文本着色器基类作为起点。我的 SyntaxRichTextBox
版本包含额外的非 XSL 特定编辑器功能(行和列信息、脏状态、代码打印...)以及一些用于增强性能和提供可重写语法高亮的更改。我在 XSLRichTextBox
中重写了 ProcessLine()
方法,调用 ProcessXmlTags()
来处理具有多个子部分、我希望为每个子部分赋予不同颜色的更复杂的 XSL 标记。
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();
}
语法高亮中使用的 RegEx
类定义在 _RegularlyExpressYourself.cs_ 文件中 :),并为了性能被编译到 _XSL.Library.RegularExpressions.dll_ 中。如果正则表达式有任何更改,我只需在 VS.NET 调试器中调用一次 GenerateRegExAssembly()
函数。主要的模式是 XSL 标记,这是一个略微修改的 XML 标记模式,其中包含所有有效 XSL 标记的列表。这样做是为了避免标记拼写错误或纯 XML 标记被错误地着色,从而指示可能存在的问题。
/// <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
}
可以使用 XSLRichTextBox
的 SyntaxSettings
属性调整语法高亮。语法高亮说明
- 一些
SyntaxSettings
目前未使用(例如,注释模式、整数、字符串) - 您在输入时,标记会逐行着色
- 设置
Text
属性会导致所有标记/行被着色 - 粘贴已着色的文本可以正常工作,但粘贴纯文本标记目前只会着色最后一行
- 着色速度不如 VS.NET 等,但对于我关闭重绘、锁定窗口更新等操作来说已经足够了。演示的 XSL 文件约有 700 行,约 28,000 个字符,在大约 2 或 3 秒内着色完成(Vista 3.16 Ghz,4 GB RAM)。
转换
可以通过 3 个位置的几个 Transform()
方法重载来调用 XSLT 函数:XSLRichTextBox
(最理想的位置)、XSLEditor
控件,或直接从 TransformUtility
静态方法调用。
通常,您会调用 XSLRichTextBox.Transform()
方法,传入 XML(XMLDocument
、FileInfo
或 XML string
)以及可选的 TransformSettings
(取决于默认设置是否满足需求)。Transform()
返回一个 TransformResults
对象,您需要处理该输出数据。
XSL 参数
一个有趣的点是可选的但默认的动态参数提示。我想要一种机制,可以将参数值传递给各种 xsl:param
标记,而无需编写代码或参数表单,从而保持一切通用。

默认情况下,每个参数都会被拉入动态表单,其中 param
名称用作标签,值输入是一个 TextBox
。表单的大小会根据参数数量进行调整,如果没有参数或 EnableParamPrompting
为 false 时则不显示。为了自定义参数提示的用户界面,我必须在 xsl:param
标记中添加一些我自己的 custom
* 属性,以便为用户界面提供控件类型、默认数据等提示。还添加了对某些 .NET 方法调用的支持,例如 Environment.MachineName
、DateTime.Now
等。您可能会说这会用自定义语法“污染”XSL 来进行转换 UI 元数据,但这正是我所需要的。同样,参数提示可以被关闭,任何值都可以手动传入。
<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" />
默认转换表单
如果您在没有参数的情况下调用 XSLRichTextBox.Transform()
,或者通过 XSLEditor
控件(例如通过工具栏)调用默认转换,您将得到一个默认对话框,允许您输入 XML 文件名、指定设置、转换并查看结果。如果您正在使用 XSLEditor
控件并且不想使用此对话框,您可以挂钩到 ActionBeforeExecute
事件,检查操作类型,执行自己的功能,并将 e.Cancel
设置为取消默认功能。

示例数据
DEMO_ORDERS.xml 是我的插件工具针对示例 Oracle XE 数据库生成的,并包含在演示解决方案中。演示加载时会使用示例 XSL(当然是在编辑器中编写的)来在运行转换时生成 Oracle 包体 SQL。SQL 输出的插件版本使用 Oracle SQL RichTextBox
(类似非官方 Oracle Developer Tools 的用法),但这又是另一回事了。
智能感知。
我几乎不敢提及“智能感知”,因为
- 我没有完成它,
- 它不是那么智能,而且
- 实现不是您想构建的基础。
事实上,智能感知默认是关闭的。我真正关心的只是弹出一个标签列表,并附带每个标签的工具提示描述。我寻找的是内置参考,而不是插入/编辑功能。基本的标签是可以插入的,但我基本上就到此为止了。某些属性会显示在某些标签上,XSL 函数也会被加载,但不会出现在智能感知中。
IntellisenseMgr
类从 XSLRichTextBox
构造函数实例化。然后 IntellisenseMgr
挂钩到 XSLRichTextBox
的更改事件,如果启用了智能感知,并且发送了相应的按键,则会调用各种设置函数。
一些初始的一次性设置包括构建智能感知数据(通过非常简单、枯燥的 POCO 类完成)、挂钩事件和设置智能感知 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);
}
一些每次调用的设置
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();
}
其他特性
- 验证 - 通过
XMLEditor
控件;这实际上是自动验证 XML,而不是 XSL。运行转换似乎是唯一完整的验证方法,但这样做过于昂贵,无法频繁执行。在 _Program Files\Microsoft Visual Studio 9.0\Xml\Schemas_ 中有一个 xslt.xsd 文件,我曾考虑过在某个时候利用它。 - 查找/替换和通用编辑 - 通过
XMLEditor
控件和XSLRichTextBox
:标准的简单查找/替换对话框、打开、保存、打印等。
关注点
范围蔓延如何吞噬我们真是很有趣。Codesmith 演变成了一个插件,而一个模板演变成了一个 XSL 编辑器项目,这也许比主要功能的其他部分花费了更多精力。我确信我也忽略了更简单的完成某些事情的方法,但这是一个有趣的学习经历。它也让我更加欣赏 VS.NET 等强大的代码编辑器。
好吧,这是我的第一篇 CodeProject 文章,所以请多指教。 :) 我试图将大部分特定于域的内容从源代码插件项目中干净地剥离到演示解决方案中。尽管我必须很快完成,所以我确信有些东西被遗漏了,很多东西也没有经过彻底测试。
历史
- 06/08/2009 - 初始版本