WPF 中的 ExpressionTree 可视化工具
一个简短的项目,用于在 WPF 中显示 LINQ Expression 树。
引言
本文讨论了一种在 WPF TreeView 中显示 LINQ Expression Tree 的优雅方式。它是两种流行的编程方式的完美结合:函数式 LINQ 和声明式 WPF。
在玩转 LINQ 表达式时,重要的是要透彻理解其工作原理。可视化树是理解的关键工具。Visual Studio 2010 有一个调试视图(显示在左侧),它以一种元语言显示表达式,这种语言对我来说并不太奏效,并且隐藏了其潜在的树状结构。标准的调试器快速视图会显示每个属性和子类,这也会分散我对本质的注意力。
包含的代码使用户能够导航 Expression Trees 的本质。它专注于那些派生自 System.Linq.Expressions.Expression
的节点。我相信,要更改实现以用户觉得有用 的任何方式显示树,应该很简单。
背景
.NET 3.0 以来,Expression Trees 一直是 .NET 的一部分。它们已经受到了一些关注,但远远没有达到应有的程度。我们中的 SQL 人通常很乐意编写代码来编写代码。我在 SQL 代码中经常使用动态 SQL,并且能够使用表达式做类似的事情。非常强大的东西。
这种让程序编写自己的技巧一直存在于 Lisp 系列编程语言中,在那里你可以将代码视为数据,并通过操作这些数据来修改程序。这个系列在世界上并没有得到很多喜爱,我认为主要是因为 波兰表达式。这种尤达语,虽然比我们学校里学到的符号更不容易产生歧义,但初次使用这些语言时会让人望而却步。被吓到的用户不会增加语言的成功。
现在,这个技巧已经通过 LINQ Expressions 来到了 .NET 平台。为了简洁起见,本文仅讨论在 WPF 中显示这些树。如果您想了解更多关于这项令人兴奋的新技术的信息,我推荐 这篇 作为入门的好起点。
使用代码
Expression Tree 中的每个节点都派生自 Expression。通常,它有几个属性包含其他 Expression,从而形成一个树。每个 Expression 节点都没有子节点集合,因此编写了一个适配器,该适配器使用反射将所有类型为 Expression
的属性投影到一个这样的集合中。以下是 ExpressionAdapter
的列表。
// Adapter to ease binding to WPF. Main goal was to create
// an IEnumerable of childnodes that the TreeView can bind to
public class ExpressionAdapter {
// private fields.
private le.Expression _Expr;
private string _ParentPropertyName;
// constructor
public ExpressionAdapter(le.Expression Expr, string ParentPropertyName) {
_Expr = Expr;
_ParentPropertyName = ParentPropertyName;
}
// Returns string with information about the current Property - Expression pair
public string Text {
get {
return _ParentPropertyName + " = " + (_Expr == null ?
"null" : (_Expr.NodeType.ToString() + " : " + _Expr.ToString()));
}
}
// Returns all properties of type Expression as an IEnumerable<expressionadapter>
public IEnumerable<expressionadapter> Children {
get {
if (_Expr == null) return null;
else
return from childExpression in _Expr.GetType().GetProperties()
where childExpression.PropertyType == typeof(le.Expression)
select new ExpressionAdapter((le.Expression)
childExpression.GetValue(_Expr, null), childExpression.Name);
}
}
}
此适配器在构造函数中接受一个类型为 Expression
的节点,然后将该节点上所有类型为 Expression
的属性公开为一个名为 Chidlren
的 IEnumerable
。WPF 的 TreeView
可以绑定到该集合来填充树。
下面是创建简单表达式并将树与之绑定的代码。
// create some bogus expression
Expression<func><int,>> expr = (x, y) =>
x > 10 ?
y > 20 ?
100
: x * (int)Math.Sqrt(x)
: (int)((x + y) * Math.PI);
// wrap expression in ExpressionAdapter and then List<expressionadapter>
// (because a Treeview.ItemSource expects an IEnumerable) and bind TreeView to it
List<expressionadapter> l = new List<expressionadapter>() {
new ExpressionAdapter(expr, "Expression") };
tvExpression.ItemsSource = l;
下面是让 TreeView
绑定到 ExpressionAdapter
的 XAML。
<Window x:Class="ExpressionsWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF Expression Visualizer" Height="350" Width="525"
xmlns:local="clr-namespace:ExpressionsWpf">
<Grid>
<TreeView Name="tvExpression" ItemsSource="={Binding}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="x:Type local:ExprWrapper"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Text}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
关注点
我惊喜地发现,只用这么少的代码就能以这种方式显示 Expression Tree。我喜欢代码的声明性,只需指定需要做什么,而不是编写循环来自己完成。实际上,我非常喜欢声明式编程,以至于当我看到老式的混乱代码,在初始赋值后有大量的赋值和状态更改时,我感到我的大脑在排斥它。很容易搞错这些事情。
这段代码中值得注意的优点是所有这些都是通过延迟评估完成的。当用户打开节点以实际查看子节点时,Children IEnumerable
才会被枚举。可以通过在 Children_get
上设置断点,然后导航树来看到这种行为。当表达式变得很大或有循环时(不确定是否可以做到,我会尝试并通知您),这是一个非常重要的优势。
我的第一个尝试是将 TreeView
绑定到 Expression
,使用 IValueConverter
和 Expession
上的一个扩展方法来公开子节点,但我切换到这种方法是因为它看起来更整洁,并且只需要一个辅助类(一个用于 IValueConverter
,一个静态类用于扩展方法)。在我的编码指南中,少即是多。
我仍然对构造函数中将 Expression
分配给局部变量感到不完全满意。在函数式编程中,状态应尽可能地保留在调用堆栈上,而不是在堆上。在这里,所有这些 ExpressionAdapter
都是对 Children_get
函数调用的结果,所以它们在堆栈上,除了顶层节点。另外,整个东西是不可变的,所以不会出现问题。代码按原样工作得很好,没有必要过于纠结。
历史
- 2010年5月24日:版本 1.0。
- 2010年5月24日:版本 1.1:清理了格式。
- 2010年3月25日:版本 1.2:用更简洁的 LINQ 查询替换了
Children_get
属性。