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






4.77/5 (11投票s)
查看 Microsoft 客户端报表技术的一种非传统用法,它可以让您的 Winform 或 ASP.NET 应用程序具备“表达式感知”能力,从而更加强大。
引言
如果您在 CodeProject 上搜索“表达式求值”,您会找到许多关于如何在 .NET 中实现此目的的优秀文章和不同技术的文章。就像我在这里提出的解决方案一样,它们都有各自的优缺点。也许在不久的将来,我们会有一个安全、快速且易于使用的解决方案,超越当今的标记解析器、CodeDOM 操作器和 DynamicMethod 示例。表达式求值甚至可能是微软正在跟进的动态语言热潮(阅读 Ruby on Rails)的副产品。您听说过 Dynamic Language Runtime 和 Lightweight Code Generation 吗?
但跑题了。我希望在我的应用程序中提供表达式求值功能,而无需编写大量代码或规避安全问题。我想要标记解析器的速度、安全性和控制力,但我想要 CodeDOM 的强大功能和丰富的语言。在本文中,我将解释如何使用 LocalReport 作为通用表达式求值类的基础,我称之为 LR-Evaluator。以下是 LR-Evaluator 功能的概述:
- 在一次调用
Eval()
中,它可以评估多达 1,584 个单独的表达式。 - 虽然 LR-Evaluator 是用 C# 编写的,但表达式是用 VB.NET 编写的。
- 表达式可以引用自定义代码。自定义代码可以是公共方法、字段或属性。
- 自定义代码和表达式默认在受限沙箱内的 Code Access Security (CAS) 下运行。这意味着文件 I/O 或网络访问将不起作用,除非您在 Evaluator 实例中显式将
ExecuteInCurrentAppDomain
设置为 True。 - 可以将参数传递给求值器,表达式可以引用这些参数。
- 可以将多个数据源传递给求值器。表达式可以引用这些数据源中的字段。数据源可以是数据表或自定义业务对象集合。
背景
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 的表达式求值何时有用?虽然很难一概而论,但报表设计器本身就是一个很好的例子。您可能会注意到它遵循以下模式:
- 您的应用程序可能有一个“设计器”或“编辑器”模块,允许用户创建某种“模板”。例如,报表模板、数据录入模板、邮件合并模板、图形模板、代码生成模板等。
- 模板中嵌入了占位符。这些占位符可能包含表达式。
- 模板在运行时进行渲染以产生结果。也就是说,表达式从模板中提取出来,进行求值,然后将结果合并回模板以生成有用的内容,例如填充的表单、要打印的内容、要执行的内容等。
使用代码
以下是来自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 表达式。当点击“预览”按钮时,应用程序将执行以下操作:
- 将当前模板保存到字符串变量
- 为“数据”文本框中当前活动的行创建数据源(新的 DataTable)。此行作为数据源传递给 LR-Evaluator 实例。
- 解析我们的表达式。对于此演示,表达式是“«”和“»”之间的任何内容。
- 将每个表达式添加到 LR-Evaluator 的 Expressions 集合中。
- 调用 LR-Evaluator 实例上的
Eval()
方法。 - 遍历 Expressions 集合,用 LastResult 值替换模板中的每个表达式。
- 进入“预览”模式并显示合并后的结果。
一旦用户处于“预览”模式,他们就可以跳到上一条或下一条记录来更改数据源并重新评估/重新合并表达式。
点击“编辑”按钮会恢复保存的模板,并允许用户修改模板或数据。
LR-Evaluator ASP.NET 演示
Web 示例应用程序允许用户输入输入参数、数据源、自定义代码和表达式,当用户点击“求值”按钮时,所有这些都将被传递给 LR-Evaluator 实例。表达式可以引用参数、数据源字段以及嵌入代码中的公共函数/字段/属性。
关注点
LR-Evaluator 幕后
LR-Evaluator 基本上是 LocalReport 现有表达式求值功能的包装器。大部分操作发生在 LR-Evaluator 的 Eval()
方法调用中。Eval()
采取以下步骤来操作 LocalReport 以获取结果:
- 在内存中生成一个 RDLC 文件,并将其作为输入流加载到 LocalReport 中。RDLC 包含所有必需的报表对象定义,例如文本框、参数名称、数据源定义和嵌入代码。为了我们的目的,我们为 LR-Evaluator 对象中的 Expressions 集合中的每个表达式生成一个文本框元素。我们的 1,584 个表达式限制是由于 LocalReport 中存在一个明显的限制(请注意,此限制仅针对表达式,而不针对可能包含字面值的文本框)。
- 我们将 LR-Evaluator 集合中的项目添加到相应的 LocalReport 集合中,例如参数值和数据源实例。
- 如果
ExecuteInCurrentAppDomain
为 True,则我们将 LocalReport 的 ExecuteReportInCurrentAppDomain 设置为当前应用程序域的证据。注意:如果您将此设置为 true 并且安全是任何关心的问题,您可能需要阅读有关 System.Security.Policy 的内容。 - 我们调用 LocalReport 的 Render() 方法,该方法将基于我们动态生成的 RDLC 数据和其他输入返回一个渲染后的报表输出流。在我们的例子中,我们告诉 LocalReport 以 Adobe PDF 格式输出。LocalReport 目前仅支持将输出流到 Excel、PDF 或图像格式。幸运的是,在我们的例子中,PDF 输出未压缩,因此它似乎是较差的选择来解析。
- 我们获取渲染后的输出流,将其转换为字符串,并解析出结果。如果 LocalReport 支持 XML 输出(如服务器端技术那样)就好了。幸运的是,PDF 是开放规范,因此找出如何解析我们的结果并不难。
- 最后,我们更新 Expressions 集合中的
EvaluatorExpression.LastResult
值。
自定义代码和外部程序集
LocalReport 类支持两种类型的自定义代码:
- VB.NET 公共函数/属性/字段,以字符串形式嵌入在 RDLC 中
- 对自定义外部程序集的引用
LR-Evaluator 可以很好地支持第一种选项。虽然我尝试支持在 LR-Evaluator 中引用外部程序集,但未能成功。网上有很多关于人们也与之搏斗的帖子。我认为问题在于 Code Access Security 和动态内存 RDLC 创建与使用物理文件进行 RDLC 的对比。我想避免为该实用程序生成物理文件,因为我打算在 Web 服务器上使用它;我不想要额外的 I/O。而且,由于安全是我使用此库的首要目标之一,因此让外部程序集工作似乎涉及到偏离受限沙箱功能,这不符合我的兴趣。
我非常感谢任何反馈或代码改进建议。谢谢您的关注!
历史
- 2007 年 5 月 7 日提交到 Code Project。