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

LR-Evaluator:基于 LocalReport 的表达式求值引擎

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (11投票s)

2007 年 5 月 8 日

CPOL

9分钟阅读

viewsIcon

226399

downloadIcon

2007

查看 Microsoft 客户端报表技术的一种非传统用法,它可以让您的 Winform 或 ASP.NET 应用程序具备“表达式感知”能力,从而更加强大。

Screenshot - LREvaluator1.png

引言

如果您在 CodeProject 上搜索“表达式求值”,您会找到许多关于如何在 .NET 中实现此目的的优秀文章和不同技术的文章。就像我在这里提出的解决方案一样,它们都有各自的优缺点。也许在不久的将来,我们会有一个安全、快速且易于使用的解决方案,超越当今的标记解析器、CodeDOM 操作器和 DynamicMethod 示例。表达式求值甚至可能是微软正在跟进的动态语言热潮(阅读 Ruby on Rails)的副产品。您听说过 Dynamic Language RuntimeLightweight Code Generation 吗?

但跑题了。我希望在我的应用程序中提供表达式求值功能,而无需编写大量代码或规避安全问题。我想要标记解析器的速度、安全性和控制力,但我想要 CodeDOM 的强大功能和丰富的语言。在本文中,我将解释如何使用 LocalReport 作为通用表达式求值类的基础,我称之为 LR-Evaluator。以下是 LR-Evaluator 功能的概述:

  1. 在一次调用 Eval() 中,它可以评估多达 1,584 个单独的表达式。
  2. 虽然 LR-Evaluator 是用 C# 编写的,但表达式是用 VB.NET 编写的。
  3. 表达式可以引用自定义代码。自定义代码可以是公共方法、字段或属性。
  4. 自定义代码和表达式默认在受限沙箱内的 Code Access Security (CAS) 下运行。这意味着文件 I/O 或网络访问将不起作用,除非您在 Evaluator 实例中显式将 ExecuteInCurrentAppDomain 设置为 True。
  5. 可以将参数传递给求值器,表达式可以引用这些参数。
  6. 可以将多个数据源传递给求值器。表达式可以引用这些数据源中的字段。数据源可以是数据表或自定义业务对象集合。

背景

Microsoft 的报表技术包含多个组件,使我们能够在 Windows Forms 和 ASP.NET 应用程序中创建强大的报表解决方案。关于这项技术可能不太清楚的一点是,它包含客户端报表引擎和服务器端报表引擎。服务器端技术,也就是众所周知的 Microsoft SQL Server Reporting Services,需要 SQL Server 并使用 Web 服务发布、管理和提供报表。客户端组件随 Visual Studio 一起提供,或者可以 下载,以便您可以创建不需要 SQL Server 或任何其他数据库的独立报表。客户端功能比其服务器端兄弟产品略有缩减,但仍然拥有我将在本文中重点介绍的所有表达式求值功能。

客户端组件主要包括 LocalReport 类、WinForm ReportViewer 控件和 ASP.NET ReportViewer 控件。这些控件在后台使用 LocalReport 类。

在完成了几个使用 Microsoft Reporting Services(上手后相当棒)的项目后,我开始想到,它拥有了我想要的表达式求值引擎的所有功能——我只是不需要“报表”部分。您会发现 LocalReport 不仅可以评估表达式,还可以通过 Code Access Security 很好地处理安全问题。其他对表达式求值引擎有用的功能包括附加自定义代码、传递参数和传递数据源。它还可以相对可靠地加载、执行和卸载。因此,在进行了一些尝试和测试后,我认为性能足以满足我的需求,并且值得整理一个项目与社区分享。

表达式求值

如果您使用过 Reporting Services,那么您可能对表达式是什么以及它们有多强大有一个很好的概念。例如,报表设计器,像所有报表设计器一样,允许您创建包含文本框和其他数据绑定容器的报表模板,这些容器在运行报表时会显示底层数据源的值。您可以在设计时填充文本框的属性,以决定文本框的内容以及诸如字体名称、字号、前景色、背景色等视觉方面。Microsoft 报表技术的一个有趣之处在于,几乎每个报表控件的每个属性都可以是代码表达式,而不仅仅是字面值。因此,文本框的前景色属性可以是一个字面值,如“Black”,也可以是一个运行时表达式,如 =IIF(Fields!Status.Value= "OUT OF STOCK", "Red","Black")

LR-Evaluator 的表达式求值何时有用?虽然很难一概而论,但报表设计器本身就是一个很好的例子。您可能会注意到它遵循以下模式:

  1. 您的应用程序可能有一个“设计器”或“编辑器”模块,允许用户创建某种“模板”。例如,报表模板、数据录入模板、邮件合并模板、图形模板、代码生成模板等。
  2. 模板中嵌入了占位符。这些占位符可能包含表达式。
  3. 模板在运行时进行渲染以产生结果。也就是说,表达式从模板中提取出来,进行求值,然后将结果合并回模板以生成有用的内容,例如填充的表单、要打印的内容、要执行的内容等。

使用代码

以下是来自Evaluator_Tests.cs 的列表,展示了如何从各个角度使用 Eval()

[Test]
public void Case_StaticSingleEval()
{
    // This sample demonstrates a simple static call to evaluate a single 
    // expression.
    // Returned value is always a string representation of result.
    // If the expression does not begin with an equals sign then it is treated
    // as a literal value which will just get returned back.
    // If the expression is invalid, no exception is thrown but the returned
    // result is "#Error".

    Debug.WriteLine(Evaluator.Eval("=today()"));
}

[Test]
public void Case_StaticMultiEval()
{
    // Multiple expressions can be passed to a static call of Eval in the form 
    // of a string array. A corresponding string array is returned which 
    // contains evaluated results.
    // The return array is always the same number of elements is the input 
    // array.
    // If any of the expressions are invalid, all results will be "#Error".

    string[] sResults = Evaluator.Eval(new string[] { "=today()", 
        "literal val", "=1+1+2" });
    foreach (string s in sResults)
        Debug.WriteLine("StaticMultiEval result = " + s);
}

[Test]
public void Case_InstanceSingleEval()
{
    // This sample demonstrates how to evaluate a single expression
    // via an instance of the Evaluator class.
    // Using an instance of the evaluator provides more control than static 
    // calls.
    // You can assign each expression a key which can be used after the Eval() 
    // to retrieve the corresponding evaluated result.
    // Note that for instance calls to Eval(), bad expressions will throw
    // trappable exceptions.
    string sResult = "";
    Evaluator ev = new Evaluator();

    ev.Expressions.Add("MyExpressionKey", new EvaluatorExpression("=today()"));
    ev.Eval();
    sResult = ev.Expressions["MyExpressionKey"].LastResult;
    Debug.WriteLine("InstanceSingleEval result = " + sResult);
}

[Test]
public void Case_InstanceMultiEval()
{
    // This sample demonstrates how to evaluate multiple expressions
    // via an instance of the Evaluator class.

    Evaluator ev = new Evaluator();


    // Note: If you want to work off boolean return value from Eval() call 
    // rather than having it throw an exception for bad expressions, uncomment 
    // the next line...
    //ev.ThrowExceptions = false; // default is true for instance calls, false 
    //for static calls

    ev.Expressions.Add("Expression1", new EvaluatorExpression("=today()"));
    ev.Expressions.Add("Expression2", 
        new EvaluatorExpression("=today().AddDays(1)"));
    ev.Expressions.Add("Expression3", 
        new EvaluatorExpression("=today().AddDays(2)"));
    ev.Eval();

    // Note: If ThrowExceptions is turned off, you can do something like 
    // this...
    //if (ev.Eval())
    // Debug.WriteLine("All expressions evaluated successfully.");
    //else
    // Debug.WriteLine("Bad expression somewhere. Inspect ev.LastException for 
    // details.");

    foreach (KeyValuePair<string, EvaluatorExpression> 
        kvp in ev.Expressions)
    {
        Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " + 
            kvp.Value.LastResult);
    }
}

[Test]
public void Case_SimpleParmEval()
{
    // Parameters can be passed to an instance of the Evaluator class.
    // An input parameter can be a string or an array of strings.
    // Parameters can be referenced in expressions like 
    // "=Parameters!MyParm.Value"

    Evaluator ev = new Evaluator();
    ev.Parameters.Add("MyParm", new EvaluatorParameter("12345"));
    ev.AddExpression("=\"Your parameter value is \" + 
        Parameters!MyParm.Value");
    ev.Eval();
    foreach (KeyValuePair<string, EvaluatorExpression> 
        kvp in ev.Expressions)
    {
        Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " + 
            kvp.Value.LastResult);
    }
}

[Test]
public void Case_ArrayParmEval()
{
    // Parameters can be passed to an instance of the Evaluator class.
    // An input parameter can be a string or an array of strings.
    // Expressions can access individual elements of a parameter that is a 
    // string array by using an index number. Arrays are zero-based.

    Evaluator ev = new Evaluator();
    ev.Parameters.Add("MyMultiParm", new EvaluatorParameter(new string[] { 
        "parmValA", "parmValB", "parmValC" }));

    // note: the following is an example of using an array index to return 
    // first item in VB array
    ev.AddExpression("=\"The first item of array parm is \" + 
        Parameters!MyMultiParm.Value(0)");

    // note: the following is an example to get all array items joined 
    // together with comma delimiter. Use Split() function to split.
    ev.AddExpression("=\"The joined array parm is \" + 
        Join(Parameters!MyMultiParm.Value, \",\")");

    ev.Eval();
    foreach (KeyValuePair<string, EvaluatorExpression> 
        kvp in ev.Expressions)
    {
        Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " + 
            kvp.Value.LastResult);
    }
}

[Test]
public void Case_DataTableEval()
{
    // Multiple data sources can be passed to an instance of the Evaluator 
    // class.
    // A data source can be either a DataTable or a business object collection.
    // Note that if you work with DataSets, you must pass an individual table 
    // of the dataset (and not the dataset) to the Evaluator instance.

    Evaluator ev = new Evaluator();
    DataTable dt = new DataTable();
    dt.Columns.Add("FirstName");
    dt.Columns.Add("LastName");
    dt.Rows.Add(new Object[] { "Ronald", "McDonald" });
    dt.Rows.Add(new Object[] { "Barney", "Rubble" });
    dt.Rows.Add(new Object[] { "Scooby", "Doo" });

    ev.AddDataSource("Customer", dt);

    // not referencing a field with a scope function will return values from 
    // the last row
    ev.AddExpression("=\"Field ref with no scope: \" + 
        Fields!FirstName.value");

    // if more than one datasource is added to localreport then a scope 
    // function required
    ev.AddExpression("=\"Field ref with First() scope: \" + 
        First(Fields!FirstName.value, \"Customer\")");

    ev.Eval();
    foreach (KeyValuePair<string, EvaluatorExpression> 
        kvp in ev.Expressions)
    {
        Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " + 
            kvp.Value.LastResult);
    }
}

[Test]
public void Case_BizObjectEval()
{
    // Multiple data sources can be passed to an instance of the Evaluator 
    // class.
    // A data source can be either a DataTable or a business object collection.
    // A business object is simply a class definition that contains public 
    // properties.
    // The properties are treated like column values. Each instance of a 
    // business object in a
    // collection is treated like a row in a table.

    Evaluator ev = new Evaluator();
    Customer customer = new Customer();

    customer.FirstName = "Fred";
    customer.LastName = "Flintstone";
    customer.Address.Addr = "301 Cobblestone Way";
    customer.Address.City = "Bedrock";
    customer.Address.State = "BC";
    customer.Address.Zip = "00001";

    List<Object> custs = new List<Object>();
    custs.Add(customer);

    ev.AddDataSource("Customer", custs);
    ev.AddExpression("=Fields!FirstName.value");

    // note how we can reference nested object properties in a biz object
    ev.AddExpression("=Fields!Address.value.Zip");

    ev.Eval();
    foreach (KeyValuePair<string, EvaluatorExpression> 
        kvp in ev.Expressions)
    {
        Debug.WriteLine(kvp.Key + ", " + kvp.Value.Expression + ", " + 
            kvp.Value.LastResult);
    }
}

[Test]
public void Case_EmbeddedCodeEval()
{
    // Embedded code is a string of public fields, properties, and/or 
    // functions in VB.NET syntax which can be referenced by expressions. The 
    // report engine compiles these members into a "Code" class. So your 
    // expressions call embedded code like "=Code.MyMethod()" or 
    // "=Code.MyField".

    string sResult = "";
    Evaluator ev = new Evaluator();
    ev.Code = "Public Function DoSomethingWith(
        ByVal s As String) as string \r\n" +
        "dim x as string = s & \"bar\" \r\n" +
        "return x \r\n" +
        "End Function \r\n";

    ev.Expressions.Add("MyExpression", 
        new EvaluatorExpression("=Code.DoSomethingWith(\"foo\")"));
    ev.Eval();
    sResult = ev.Expressions["MyExpression"].LastResult;
    Debug.WriteLine("EmbeddedCodeEval result = " + sResult);
}

[Test]
public void Case_ComplexEval()
{
    // This example demonstrates how to use an instance of the Evaluator class 
    // to evaluated multiple expressions referencing parameters, data sources
    // (data table and biz object).

    Evaluator ev = new Evaluator();

    // add a couple parameters...

    // a parameter can be a single string value
    ev.Parameters.Add("Parm1", new EvaluatorParameter("***Parm1 value***"));
    // or a parameter can be an array of strings
    ev.Parameters.Add("Parm2", new EvaluatorParameter(new string[] { 
        "parm2.a", "parm2.b", "parm2.c" }));

    // now create a couple of datasources for our evaluator...

    // first make and add a list of customer objects...
    List<Object> TestCustomers = new List<Object>();
    TestCustomers.Add(new Customer("John", "Doe"));
    TestCustomers.Add(new Customer("Jane", "Smith"));
    ev.AddDataSource("BizObjectCollection", TestCustomers);

    // now make and add a standard datatable
    DataTable dt = new DataTable("MyTable");
    dt.Columns.Add("FirstName");
    dt.Columns.Add("LastName");
    dt.Rows.Add(new Object[] { "Ronald", "McDonald" });
    dt.Rows.Add(new Object[] { "Fred", "Flintstone" });
    ev.AddDataSource("SomeDataTable", dt);

    // now add some expressions to be evaluated...
    ev.Expressions.Add("Expression1", new EvaluatorExpression(
        "=\"Today is: \" + Today().ToLongDateString()"));
    ev.Expressions.Add("Expression2", new EvaluatorExpression(
        "=\"Tomorrow is: \" + Today().AddDays(1).ToLongDateString()"));
    ev.Expressions.Add("Expression3", new EvaluatorExpression("=(1+1)*(2+2)"));
    ev.Expressions.Add("Expression4", new EvaluatorExpression(
        "=\"Value of first passed-in parameter is: \" + 
        Parameters!Parm1.Value"));
    ev.Expressions.Add("Expression5", new EvaluatorExpression(
        "=\"Value of second passed-in parameter (second element) is: \" + 
        Parameters!Parm2.Value(1)"));
    ev.Expressions.Add("Expression6", new EvaluatorExpression(
        "=\"FirstName field value from biz object is: \" + 
        First(Fields!FirstName.value, \"BizObjectCollection\")"));
    ev.Expressions.Add("Expression7", new EvaluatorExpression("=\"FirstName 
        field value from datatable row is: \" + First(Fields!FirstName.value, 
        \"SomeDataTable\")"));

    // do the evaluating...
    ev.Eval();
    foreach (KeyValuePair<string, EvaluatorExpression> 
        ee in ev.Expressions)
    {
        System.Diagnostics.Debug.WriteLine("-----------------------------");
        System.Diagnostics.Debug.WriteLine("Expression key: " + ee.Key);
        System.Diagnostics.Debug.WriteLine("Expression string: " + 
            ee.Value.Expression);
        System.Diagnostics.Debug.WriteLine("Expression last result: " + 
            ee.Value.LastResult);
    }
}

LR-Evaluator 邮件合并演示

邮件合并示例应用程序允许用户输入自由格式文本,其中包含可以求值并与数据源记录合并的嵌入式 VB.NET 表达式。它稍微类似于 Microsoft Word 等文字处理器中的邮件合并功能。顶部的文本框是输入或粘贴 CSV 格式数据的地方。这些数据被加载到一个 DataTable 中,我们将使用它来为 LR-Evaluator 的实例创建按行指定的数据源。底部的文本框用于编辑窗体信函模板。可以根据需要插入表达式,并且这些表达式可以包含引用数据源字段的 VB.NET 表达式。当点击“预览”按钮时,应用程序将执行以下操作:

  1. 将当前模板保存到字符串变量
  2. 为“数据”文本框中当前活动的行创建数据源(新的 DataTable)。此行作为数据源传递给 LR-Evaluator 实例。
  3. 解析我们的表达式。对于此演示,表达式是“«”和“»”之间的任何内容。
  4. 将每个表达式添加到 LR-Evaluator 的 Expressions 集合中。
  5. 调用 LR-Evaluator 实例上的 Eval() 方法。
  6. 遍历 Expressions 集合,用 LastResult 值替换模板中的每个表达式。
  7. 进入“预览”模式并显示合并后的结果。

Screenshot - LREvaluator2.png

一旦用户处于“预览”模式,他们就可以跳到上一条或下一条记录来更改数据源并重新评估/重新合并表达式。

Screenshot - LREvaluator3.png

点击“编辑”按钮会恢复保存的模板,并允许用户修改模板或数据。

LR-Evaluator ASP.NET 演示

Web 示例应用程序允许用户输入输入参数、数据源、自定义代码和表达式,当用户点击“求值”按钮时,所有这些都将被传递给 LR-Evaluator 实例。表达式可以引用参数、数据源字段以及嵌入代码中的公共函数/字段/属性。

Screenshot - LREvaluator4.png

关注点

LR-Evaluator 幕后

LR-Evaluator 基本上是 LocalReport 现有表达式求值功能的包装器。大部分操作发生在 LR-Evaluator 的 Eval() 方法调用中。Eval() 采取以下步骤来操作 LocalReport 以获取结果:

  1. 在内存中生成一个 RDLC 文件,并将其作为输入流加载到 LocalReport 中。RDLC 包含所有必需的报表对象定义,例如文本框、参数名称、数据源定义和嵌入代码。为了我们的目的,我们为 LR-Evaluator 对象中的 Expressions 集合中的每个表达式生成一个文本框元素。我们的 1,584 个表达式限制是由于 LocalReport 中存在一个明显的限制(请注意,此限制仅针对表达式,而不针对可能包含字面值的文本框)。
  2. 我们将 LR-Evaluator 集合中的项目添加到相应的 LocalReport 集合中,例如参数值和数据源实例。
  3. 如果 ExecuteInCurrentAppDomain 为 True,则我们将 LocalReport 的 ExecuteReportInCurrentAppDomain 设置为当前应用程序域的证据。注意:如果您将此设置为 true 并且安全是任何关心的问题,您可能需要阅读有关 System.Security.Policy 的内容。
  4. 我们调用 LocalReport 的 Render() 方法,该方法将基于我们动态生成的 RDLC 数据和其他输入返回一个渲染后的报表输出流。在我们的例子中,我们告诉 LocalReport 以 Adobe PDF 格式输出。LocalReport 目前仅支持将输出流到 Excel、PDF 或图像格式。幸运的是,在我们的例子中,PDF 输出未压缩,因此它似乎是较差的选择来解析。
  5. 我们获取渲染后的输出流,将其转换为字符串,并解析出结果。如果 LocalReport 支持 XML 输出(如服务器端技术那样)就好了。幸运的是,PDF 是开放规范,因此找出如何解析我们的结果并不难。
  6. 最后,我们更新 Expressions 集合中的 EvaluatorExpression.LastResult 值。

自定义代码和外部程序集

LocalReport 类支持两种类型的自定义代码:

  1. VB.NET 公共函数/属性/字段,以字符串形式嵌入在 RDLC 中
  2. 对自定义外部程序集的引用

LR-Evaluator 可以很好地支持第一种选项。虽然我尝试支持在 LR-Evaluator 中引用外部程序集,但未能成功。网上有很多关于人们也与之搏斗的帖子。我认为问题在于 Code Access Security 和动态内存 RDLC 创建与使用物理文件进行 RDLC 的对比。我想避免为该实用程序生成物理文件,因为我打算在 Web 服务器上使用它;我不想要额外的 I/O。而且,由于安全是我使用此库的首要目标之一,因此让外部程序集工作似乎涉及到偏离受限沙箱功能,这不符合我的兴趣。

我非常感谢任何反馈或代码改进建议。谢谢您的关注!

历史

  • 2007 年 5 月 7 日提交到 Code Project。
© . All rights reserved.