65.9K
CodeProject 正在变化。 阅读更多。
Home

一个单文件函数解析器,用于 WPF、Silverlight 和 SQL Server CLR

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2013 年 4 月 30 日

CPOL

5分钟阅读

viewsIcon

18840

downloadIcon

337

一个轻量级的单文件函数解析器,使用类似 Excel 的语法。

引言

前一段时间,我在处理经典的三层架构时遇到一个有趣的问题。我发现每一层都非常相似地需要动态地扩展 XML 和 HTML 数据,用来自数据库和客户输入中的变量和计算值。这自然而然地引出了使用类似数学解析器的想法。理想情况下,这个解析器应该能够处理像“IIF([var1] > 5, ‘OK’, ‘To Low’)”这样的表达式,以便普通 **Excel** 用户能够轻松理解和使用。

“没问题,”我想,“我一定能在我最喜欢的编程门户 CodeProject 上找到一个简洁且现成的解决方案,”但是……

在花了一些时间测试了三个非常有前景的解决方案的代码后,我总是遇到同样的几个关键问题。

  • 我的要求是,在所有层中**精确地**使用相同的代码,以确保应用程序的**所有**部分行为一致,而无需在进行小的更改后反复测试。这意味着相同的代码应该在不作任何修改的情况下,适用于 SQL Server CLR、WPF 服务、WPF 客户端和 Silverlight 客户端。
  • 我不想使用一个完整的解析“框架”或额外的程序集,而是想要一个小巧的解决方案,最好是单个文件。 

将现有解决方案改编以满足这些要求非常耗时。因此,尤其因为不需要一个功能齐全的数学解析器,我决定编写自己的简单函数解析器,并与社区分享我的经验。

背景

此实现的目的是:

  • 将**所有**功能保留在**单个文件**中,以便在不同项目之间轻松共享。 
  • 完全兼容 **WPF**、**Silverlight** 和 **SQL Server CLR** 应用程序。 
  • 使用简单的函数语法,这样具有 **EXCEL** 技能的普通用户就能理解。 
  • 能够轻松地使用**XML 路径查询**。 

此实现的**目的不是**:

  • 成为一个功能齐全的**数学解析器**。 
  • 追求解析数百万行数据的**速度记录**。 
  • 用其**复杂性**和**大量的函数**给人留下深刻印象。 

使用代码

您需要做的就是将 *FunctionParser.cs* 添加到您的项目中,所以让我们从一个简单的例子开始。

手动添加变量  

创建一个字典

Dictionary<string, object> variables = new Dictionary<string, object>();

添加一些值

variables.Add("doubleVar1", 3.0);
variables.Add("doubleVar2", 7.5);

然后就可以高兴地使用了

string result = FunctionParser.Parse("The sum of [doubleVar1] and " + 
      "[doubleVar2] is {SUM([doubleVar1], [doubleVar2])}\n",  variables);

您将获得字符串“[doubleVar1] 和 [doubleVar2] 的和是 10.5”。

使用 XML 数据 (1) 

现在我们可以尝试一个更复杂的例子了。假设我们有一些来自数据库的 XML 数据存储在一个名为 `customData` 的 `string` 变量中,我们想用它来填写一个表单信件。

<Columns>
    <Contact>
        <Column Id="Gender">Female</Column>
        <Column Id="FirstName">Carol</Column>
        <Column Id="LastName">Holland</Column>
    </Contact>
    <Address>
        <Column Id="StreetNumber">456</Column>
        <Column Id="Street">School Road</Column>
        <Column Id="ZIP">GA 50001</Column>
        <Column Id="City">Marietta</Column>
    </Address>
    <Company>
        <Column Id="Position">Director of Education</Column>
        <Column Id="Name">The Wontimal School</Column>
    </Company>
</Columns>

那么我们如何让它与我们的函数解析器一起工作呢?为了简化,有一个名为 `XmlVariableContainer` 的类。

XmlVariableContainer container = new XmlVariableContainer(customData, "//Columns//Column", true);

第一个参数是我们的 XML 数据。用作变量的元素必须有一个 Id 属性,因为我们需要一个变量键,但它们不必一定称为“columns”。第二个参数是一个 XPath 查询,用于枚举这些元素。最后一个参数决定父节点名称是否也将用作键的一部分。如果此参数设置为 `true`,我们可以使用 `[Parent.child]` 符号访问生成的变量,这样文本的可读性更强。

`XmlVariableContainer` 使用一个简单的 `Dictionary`,所以我们可以这样访问生成的值:

string firstName = container.Variables["Contact.FirstName"];

现在我们可以再次轻松地使用解析器了。

string sampleLetter = File.ReadAllText(@"Debug\Form Letter Example\SampleLetter.html");
string result = FunctionParser.Parse(sampleLetter, container.GetValue); 

使用 XML 数据 (2) 

有时 `<column Id=“..“ >` 符号很不实用,例如当您想从数据库中提取 XML 数据时(参见“SQL Server CLR 示例”)。在这种情况下,我们也可以使用以下符号:

<Data>
    <Contact>
        <Gender>Male</Gender>
        <FirstName>Carol</FirstName>
        <LastName>Holland</LastName>
    </Contact>
    <Address>
        <StreetNumber>456</StreetNumber>
        <Street>School Road</Street>
        <ZIP>GA 50001</ZIP>
        <City>Marietta</City>
    </Address>
    <Company>
        <Position>Director of Education</Position>
        <Name>The Wontimal School</Name>
    </Company>
</Data>

要获得与上一个示例(*“使用 XML 数据 1”*)相同的结果,我们需要稍微改变 `XmlVariableContainer` 的构造。

XmlVariableContainer container = new XmlVariableContainer(customData, "Data//*//*", true);

使用带 XML 变量内容的 XML 数据

到目前为止,我们讨论的是通过 XML 生成字符串或数字等变量,但如果我们需要的变量内容是 XML 数据呢?

<Variables>
    <Column Id="DoubleValue1">12.33</Column>
    <Column Id="DoubleValue2">0.5</Column>
    <Column Id="XmlValue" Type="xml">
        <Rows>
            <Row>
                <Column Id="Gender">Female</Column>
                <Column Id="FirstName">Carol</Column>
                <Column Id="LastName">Holland</Column>
                <Column Id="StreetNumber">456</Column>
                <Column Id="Street">School Road</Column>
                <Column Id="ZIP">GA 50001</Column>
                <Column Id="City">Marietta</Column>
            </Row>
            <Row>
                <Column Id="Gender">Male</Column>
                <Column Id="FirstName">John</Column>
                <Column Id="LastName">James</Column>
                <Column Id="StreetNumber">22</Column>
                <Column Id="Street">Maple Street</Column>
                <Column Id="ZIP">11111</Column>
                <Column Id="City">Independence</Column>
            </Row>
        </Rows>
    </Column>
</Variables>

要生成 XML 内容而不是文本或数字,只需添加 `Type="xml"` 属性。这个变量现在可以轻松地用作 XML 函数 `XQUERY()` 和 `XVALUE()` 的参数。

XVALUE([XmlValue], 'Rows/Row/Column[@Id=\"FirstName\"]/text()')

这会导致字符串数组 {„Carol“, „John“}。

SQL Server CLR 示例

该解析器可以轻松地与 `XmlVariableContainer` 类一起使用,创建一个简单的评估函数。

[Microsoft.SqlServer.Server.SqlFunction]
public static SqlChars Evaluate(SqlChars text, SqlXml columns, 
       SqlString columnPath, SqlBoolean useParentIdentifier)
{
    if (!text.IsNull && !columns.IsNull && !columnPath.IsNull)
    {
        bool useIdentifier = useParentIdentifier.IsNull || useParentIdentifier.IsFalse ? false : true;
        XmlVariableContainer container = new XmlVariableContainer();
        XDocument columnsDocument = XDocument.Load(columns.CreateReader(), LoadOptions.None);
        container.AddColumns(columnsDocument.XPathSelectElements(columnPath.ToString()), useIdentifier);
        return new SqlChars(FunctionParser.Parse(text.ToSqlString().ToString(), 
                            container.GetValue).ToCharArray());
    }
    return new SqlChars();
}

假设我们有一个名为“Contact”的表,至少包含 [Salutation]、[FirstName] 和 [LastName] 列,我们可以使用 FOR XML 语法创建所需的 XML 数据。

declare @Contacts xml = 
    (SELECT
        [Salutation]
        ,[FirstName]
        ,[LastName]
    FROM
        [Data].[Contact]
    FOR XML PATH('Contact'), ROOT('Contacts'))

现在我们可以调用先前创建的 Sql 函数。

declare @formLetter nvarchar(max) = '...'
SELECT [Common].[dbo].[Evaluate](@formLetter, @Contacts, 'Contacts/Contact[1]/*', 0)

 您将在演示应用程序启动时找到更多示例和完整的参考文档。

添加新函数 

我试图让添加新函数尽可能容易。所以,假设我们需要一个像 ISEMPTY() 这样的函数,它用来判断传入的参数是 null 还是空。这可以通过将以下代码添加到 `FunctionParser.Function` 类来实现。

// class FunctionParser. Function

/* Public Evaluation Methods */
...

[ParserFunction]
public bool ISEMPTY()
{
    if (this.Parameters.Length == 1)
    {
        this.Value = string.IsNullOrEmpty(this.Parameters[0].StringValue);
        return true;
    }
    return false;
}

所有解析器函数都用 `[ParserFunctionAttribute]` 标记,如果解析成功则返回 `true`,否则返回 `false`。可以通过 `Parameters` 数组访问参数。返回值可以通过赋值 `Value` 属性来设置(这有点像 VBA 语法)。

在我们的例子中,实现仅限于检查 `Parameters` 的长度(应该正好是 1),并在相应字符串为 null 或空时将 `Value` 参数设置为 `true`。

历史

  • 2013/04/26:发布初始版本。
© . All rights reserved.