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

WPF 中的 ExpressionTree 可视化工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (8投票s)

2010年5月24日

CPOL

4分钟阅读

viewsIcon

37823

downloadIcon

414

一个简短的项目,用于在 WPF 中显示 LINQ Expression 树。

引言

本文讨论了一种在 WPF TreeView 中显示 LINQ Expression Tree 的优雅方式。它是两种流行的编程方式的完美结合:函数式 LINQ 和声明式 WPF。

在玩转 LINQ 表达式时,重要的是要透彻理解其工作原理。可视化树是理解的关键工具。Visual Studio 2010 有一个调试视图(显示在左侧),它以一种元语言显示表达式,这种语言对我来说并不太奏效,并且隐藏了其潜在的树状结构。标准的调试器快速视图会显示每个属性和子类,这也会分散我对本质的注意力。

包含的代码使用户能够导航 Expression Trees 的本质。它专注于那些派生自 System.Linq.Expressions.Expression 的节点。我相信,要更改实现以用户觉得有用 的任何方式显示树,应该很简单。

DebugView.jpg

WpfTreeview.jpg

背景

.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 的属性公开为一个名为 ChidlrenIEnumerable。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,使用 IValueConverterExpession 上的一个扩展方法来公开子节点,但我切换到这种方法是因为它看起来更整洁,并且只需要一个辅助类(一个用于 IValueConverter,一个静态类用于扩展方法)。在我的编码指南中,少即是多。

我仍然对构造函数中将 Expression 分配给局部变量感到不完全满意。在函数式编程中,状态应尽可能地保留在调用堆栈上,而不是在堆上。在这里,所有这些 ExpressionAdapter 都是对 Children_get 函数调用的结果,所以它们在堆栈上,除了顶层节点。另外,整个东西是不可变的,所以不会出现问题。代码按原样工作得很好,没有必要过于纠结。

历史

  • 2010年5月24日:版本 1.0。
  • 2010年5月24日:版本 1.1:清理了格式。
  • 2010年3月25日:版本 1.2:用更简洁的 LINQ 查询替换了 Children_get 属性。
© . All rights reserved.