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

LINQ to Tree - 查询树状结构的通用技术

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (105投票s)

2010年3月2日

CPOL

14分钟阅读

viewsIcon

399686

downloadIcon

3164

本文介绍了一种将LINQ查询应用于树状结构的通用方法。利用T4模板进行代码生成,构建了LINQ to VisualTree (WPF)、LINQ to WinForms和LINQ to FileSystem API。

目录

概述

本文以LINQ to XML为例,探讨了如何将LINQ应用于树状数据。构建了一种通用的树查询方法,通过实现简单的适配器类,可以使任何树结构“兼容”LINQ。这一思路通过T4模板的代码生成进一步扩展,并提供了LINQ to VisualTree (WPF / Silverlight)、LINQ to WinForms和LINQ to Filesystem的示例。

tree.jpg

引言

C#编程语言中我最喜欢的功能之一就是LINQ。作为程序员,我们所做的大部分工作都是编写代码来过滤、操作和转换数据。LINQ带来了丰富的功能,让您能够使用更具表现力和简洁的语言来完成这些任务。它显然在语言本身的发展中发挥了重要作用,带来了扩展方法、yield和Lambda表达式。

当我开始学习WPF时,我认为将LINQ应用于可视化树来查询用户界面(UI)的状态可能很有用。对于不熟悉WPF的各位,可视化树是用于构建UI的元素、边框、网格等的层次结构。这些通常在XAML中定义,即XML,XML本身也是一种层次结构。问题在于,LINQ依赖于IEnumerable接口,该接口用于定义一维项目数组。如何用它来查询WPF可视化树这样的层次结构呢?

快速搜索Google后,我偶然发现了Peter McGrattan的LINQ to Visual Tree实现,它只是将树展平成一维结构。在其他地方,我也找到了David Jade的一个更通用的实现,可以用来展平任何树以应用LINQ查询。然而,在展平树时,您会丢失可能与您的查询相关的重要信息。它们都不是我想要的解决方案!

总之,我将这个问题搁置了一段时间,直到最近开始了一个新项目,我们在处理大量XML数据,自然而然地,我们转向了LINQ to XML...

LINQ to XML 简介

本节简要介绍了LINQ to XML API及其与XPath的关系。如果您已经是LINQ to XML方面的专家,请随时跳过,直接进入重点!

LINQ to XML API提供了查询已加载到内存中的XML文档的方法,这些文档以XDocument(或简称X-Dom)的形式存在。内存中的XML文档由节点构成树状结构。在每个节点上,都有LINQ方法可用于搜索树,以查找与当前节点(上下文)具有某种关系的子节点、同级节点或节点,这些节点匹配各种条件。

下面提供了一个基于以下XML实例文档的简单示例,该示例查询以下XML:

xml.png

以下查询将查找所有颜色子元素值为“Red”的“product”元素。

linq.png

查询的每个部分匹配的节点在上面的XML文档中已高亮显示。

查询的第一部分从文档的根开始,调用XDocument.Descendants方法查找文档中名称为order的任何元素,这些元素是树中的后代(直接或间接)。

查询的第二部分匹配order元素的所有product后代;然而,有趣的事情从这里开始。虽然上面查询中的第一个Descendants方法定义在XDocument上,但紧随其后的第二个Descendants方法则不是。此方法定义在IEnumerable<XElement>上,它是通过调用前面Descendant查询返回的集合中每个XElement上的Elements方法的结果。

LINQ to XML为单个XElement(或XDocument)上的每个常规查询方法都定义了相应的'IEnumerable' 扩展方法。正是这些方法赋予了LINQ to XML真正的强大功能,让查询能够“爬行”整个文档。

查询的最后一部分过滤product元素。它查找颜色元素,这是一个直接的后代,并找到值为Red的元素。

LINQ to XML API强大且具有表现力,但这些匹配同级、祖先、后代元素的查询方法是如何实现的呢?为此,LINQ to XML开发人员借鉴了现有的XML查询技术,即XPath。上面LINQ中的查询等效的XPath查询是:

//order//product[colour='Red'] 

XPath允许您通过组合多个表达式和谓词来创建查询,每个表达式都作用于应用前面表达式的结果节点集。这与上面描述的LINQ to XML执行类似。

使用XPath,在XML文档的特定节点上,您可以选择查询多个不同的“轴”,每个轴定义一组与当前上下文节点相关的节点。这允许您定义向不同方向遍历文档的查询。下面总结了LINQ和XPath共有的轴:

LINQ to XML XPath 图示
后代 后代 (//) image001.png
祖先 祖先 image002.png
元素 子节点 (/) image003.png
同级前元素 preceding-sibling image004.png
同级后元素 following-sibling image005.png

注意:在上面的图示中,以红色高亮显示的编码是当前上下文。以绿色高亮显示的节点是给定轴的成员。此外,XPath为上面给出的每个轴都有一个对应的“or-self”轴,它包括上下文节点;这些也在LINQ to XML中作为“AndSelf”方法存在。

LINQ to Tree 的通用实现

LINQ to XML借鉴了XPath定义的轴来查询XML树。为什么不使用这些相同的轴来查询其他树状结构呢?

问题在于,没有定义树结构的通用接口;例如,WPF可视化树和文件系统API具有非常不同的接口。解决此问题的常用方法是应用Gang of Four的适配器模式,即定义您真正想要的接口,然后“适配”现有类,使其实现该接口,通常通过将其“包装”在另一个类中来实现。

为了遍历树结构,在每个节点上,您只需要导航到父节点或子节点的方法。以下通用接口提供了遍历类型为“T”的对象树的方法:

/// <summary>
/// Defines an adapter that must be implemented in order to use the LinqToTree
/// extension methods
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ILinqToTree<T>
{
    /// <summary>
    /// Obtains all the children of the Item.
    /// </summary>
    /// <returns></returns>
    IEnumerable<ILinqToTree<T>> Children();

    /// <summary>
    /// The parent of the Item.
    /// </summary>
    ILinqToTree<T> Parent { get; }

    /// <summary>
    /// The item being adapted.
    /// </summary>
    T Item { get; }
}

此接口允许我们为类型为“T”的特定节点的各个轴创建扩展方法。

/// <summary>
/// Returns a collection of descendant elements.
/// </summary>
public static IEnumerable<ILinqToTree<T>> 
       Descendants<T>(this ILinqToTree<T> adapter)
{
    foreach (var child in adapter.Children())
    {
        yield return child;

        foreach (var grandChild in child.Descendants())
        {
            yield return grandChild;
        }
    }
}

/// <summary>
/// Returns a collection of ancestor elements.
/// </summary>
public static IEnumerable<ILinqToTree<T>> 
       Ancestors<T>(this ILinqToTree<T> adapter)
{
    var parent = adapter.Parent;
    while (parent != null)
    {
        yield return parent;
        parent = parent.Parent;
    }
}

/// <summary>
/// Returns a collection of child elements.
/// </summary>
public static IEnumerable<ILinqToTree<T>> 
       Elements<T>(this ILinqToTree<T> adapter)
{
    foreach (var child in adapter.Children())
    {
        yield return child;
    }
}

(我省略了“AndSelf”的实现;这些非常简单;如果您有兴趣,请下载本文的示例代码。)

然而,正如我们在上面的LINQ to XML部分所看到的,这些方法使我们能够从一个节点导航到其相关轴内的节点;然而,正是IEnumerable扩展方法提供了LINQ to XML的真正强大功能,使您的查询能够“爬行”整个文档。

上面各轴方法对应的IEnumerable版本如下:

/// <summary>
/// Applies the given function to each of the items in the supplied
/// IEnumerable.
/// </summary>
private static IEnumerable<ILinqToTree<T>>
        DrillDown<T>(this IEnumerable<ILinqToTree<T>> items,
        Func<ILinqToTree<T>, IEnumerable<ILinqToTree<T>>> function)
{
    foreach (var item in items)
    {
        foreach (ILinqToTree<T> itemChild in function(item))
        {
            yield return itemChild;
        }
    }
}

/// <summary>
/// Returns a collection of descendant elements.
/// </summary>
public static IEnumerable<ILinqToTree<T>> 
       Descendants<T>(this IEnumerable<ILinqToTree<T>> items)
{    
    return items.DrillDown(i => i.Descendants());
}

/// <summary>
/// Returns a collection of ancestor elements.
/// </summary>
public static IEnumerable<ILinqToTree<T>> 
       Ancestors<T>(this IEnumerable<ILinqToTree<T>> items)
{
    return items.DrillDown(i => i.Ancestors());
}

/// <summary>
/// Returns a collection of child elements.
/// </summary>
public static IEnumerable<ILinqToTree<T>> 
       Elements<T>(this IEnumerable<ILinqToTree<T>> items)
{
    return items.DrillDown(i => i.Elements());
}

(同样,“AndSelf”的实现已被省略。)

在上面的代码中,您可以清楚地看到作用于单个节点的​​方法与其执行相同功能的IEnumerable等效方法之间的关系。在上面的代码中,它们都通过私有的DrillDown方法实现,该方法将给定函数应用于集合中的每个项,并产生结果。

可以通过实现ILinqToTree接口来包装任何树结构,从而使用此API进行查询。为了实际了解ILinqToTree接口的用法,我们将创建一个适配器,以便查询WPF可视化树。

/// <summary>
/// An adapter for DependencyObject which implements ILinqToTree in
/// order to allow Linq queries on the visual tree
/// </summary>
public class VisualTreeAdapter : ILinqToTree<DependencyObject>
{
    private DependencyObject _item;

    public VisualTreeAdapter(DependencyObject item)
    {
        _item = item;
    }

    public IEnumerable<ILinqToTree<DependencyObject>> Children()
    {
        int childrenCount = VisualTreeHelper.GetChildrenCount(_item);
        for (int i = 0; i < childrenCount; i++)
        {
            yield return new VisualTreeAdapter(VisualTreeHelper.GetChild(_item, i));
        }
    }

    public ILinqToTree<DependencyObject> Parent
    {
        get
        {
            return new VisualTreeAdapter(VisualTreeHelper.GetParent(_item));
        }
    }

    public DependencyObject Item
    {
        get
        {
            return _item;
        }
    }
}

可视化树由DependencyObject实例组成,并且可以通过VisualTreeHelper确定它们之间的关系。有了上面的适配器,我们现在就可以将LINQ查询应用于可视化树了。以下是一个简单示例,其中查询了具有以下XAML标记的Window

<Window x:Class="LinqToTreeWPF.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" 
    Width="300" ContentRendered="Window_ContentRendered">

    <Grid x:Name="GridOne" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBox x:Name="TextBoxOne" 
          Text="One" Grid.Row="0" />

        <StackPanel x:Name="StackPanelOne" Grid.Row="1">
            <TextBox x:Name="TextBoxTwo" Text="Two" />

            <Grid x:Name="GridTwo">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>

                <TextBox x:Name="TextBoxThree" 
                   Text="Three" Grid.Row="0" />

                <StackPanel x:Name="StackPanelTwo" Grid.Row="1">
                    <TextBox x:Name="TextBoxFour" Text="Four"/>
                    <RadioButton>Radio</RadioButton>
                </StackPanel>
            </Grid>
            
            <ListBox>
                <ListBoxItem>Foo</ListBoxItem>
                <ListBoxItem>Bar</ListBoxItem>
            </ListBox>
        </StackPanel>
    </Grid>
</Window>

以下示例演示了针对可视化树执行的几个查询。每个示例都以LINQ查询语法和扩展方法(或流畅)语法提供:

// get all the TextBox's which have a Grid as direct parent
var itemsFluent = new VisualTreeAdapter(this).Descendants()
                                             .Where(i => i.Parent is Grid)
                                             .Where(i => i.Item is TextBox)
                                             .Select(i => i.Item);

var itemsQuery = from v in new VisualTreeAdapter(this).Descendants()
                 where v.Parent is Grid && v.Item is TextBox
                 select v.Item;

// get all the StackPanels that are within another StackPanel visual tree
var items2Fluent = new VisualTreeAdapter(this).Descendants()
                                              .Where(i => i.Item is StackPanel)
                                              .Descendants()
                                              .Where(i => i.Item is StackPanel)
                                              .Select(i => i.Item);

var items2Query = from i in
                      (from v in new VisualTreeAdapter(this).Descendants()
                       where v.Item is StackPanel
                       select v).Descendants()
                  where i.Item is StackPanel
                  select i.Item;

这些查询中的每一个都遵循相似的模式。首先,可视化树的根被包装在VisualTreeAdapter(实现ILinqToTree)中;然后是查询本身;最后,使用Select子句解包满足我们查询标准的每个项。

在我看来,对树结构的查询通常在流畅/扩展方法语法中更具可读性,查询语法中所需的子选择会不必要地冗长。然而,在少数情况下,查询语法或两者的混合提供了最易读的查询,我们稍后将看到。

虽然这种机制运行良好,但也有一些不太令人满意的地方。首先,添加适配器意味着我们不断地包装和解包树的节点。第二个问题稍微微妙一些。如果能为按类型过滤轴的常见任务添加扩展方法会很好,例如,允许我们找到类型为“TextBox”的后代。以下应该能满足要求:

/// <summary>
/// Returns a collection of descendant elements.
/// </summary>
public static IEnumerable<ILinqToTree<T>> 
       Descendants<T, K>(this ILinqToTree<T> adapter)   
{
    return adapter.Descendants().Where(i => i.Item is K);
}

上面的方法确实产生了期望的结果,通过给定类型K过滤后代节点。然而,虽然现有方法有一个可以由编译器推断的单个类型参数,但上面的方法有两个类型参数,并且推断不再发生。这意味着我们不仅必须指定要搜索的类型,还要指定正在查询的类型,如下所示:

// find all descendant text boxes
var textBoxes = new VisualTreeAdapter(this).Descendants<DependencyObject, TextBox>();

如果有人能建议一个没有这个问题的扩展方法,我很想知道!请在下方留言...

LINQ to Tree API 的代码生成

为了消除包装/解包树节点的需要,扩展方法需要定义在节点本身上。问题是,如果我们在特定的节点类型上定义扩展方法,这意味着LINQ to Tree API必须为每个节点/树类型手动构建。我找到的解决方案是“内部化”适配器的使用,通过T4模板生成LINQ to Tree扩展方法。

T4模板内置于Visual Studio中,并通过C#和简单的模板标记提供代码文件生成机制。有关编写T4模板的介绍,请尝试CodeProject上的这篇文章。解决方案如下:有一个T4模板文件,其中包含我们简化的ILinqToTree接口(不需要用于解包的Item属性或工厂方法),以及LINQ to Tree扩展方法及其IEnumerable等效方法。此代码包含在一个T4模板方法中,该方法接受参数,这些参数详细说明了正在为其生成API的类型(例如,DependencyObjectDirectoryInfo)以及为此类型实现IlinqToTree接口的类。

这是模板(注意:在此代码片段中,我仅包含后代轴;其他轴及其“AndSelf”对应项都包含在此文章相关的代码中)

<#@ template language="C#" #>

using System.Linq;
using System.Collections.Generic;
using System;
 
<#+

private void GenerateLinqMethods(string typeName, 
        string adapterType, string targetNamespace)
{
#>
namespace <#=targetNamespace#>
{
    /// <summary>
    /// Defines an interface that must be implemented
    /// to generate the LinqToTree methods
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ILinqTree<T>
    {
        IEnumerable<T> Children();

        T Parent { get; }
    }
  
    public static class TreeExtensions
    {
        /// <summary>
        /// Returns a collection of descendant elements.
        /// </summary>
        public static IEnumerable<<#=typeName#>> 
               Descendants(this <#=typeName#> item)
        {
            ILinqTree<<#=typeName#>> adapter = 
                        new <#=adapterType#>(item);
            foreach (var child in adapter.Children())
            {
                yield return child;

                foreach (var grandChild in child.Descendants())
                {
                    yield return grandChild;
                }
            }
        }    
        
        /// <summary>
        /// Returns a collection of descendant elements which match the given type.
        /// </summary>
        public static IEnumerable<<#=typeName#>> 
               Descendants<T>(this <#=typeName#> item)
        {
            return item.Descendants().Where(i => i is T).Cast<<#=typeName#>>();
        }
    }
    
    public static class EnumerableTreeExtensions
    {
        /// <summary>
        /// Applies the given function to each of the items in the supplied
        /// IEnumerable.
        /// </summary>
        private static IEnumerable<<#=typeName#>> 
            DrillDown(this IEnumerable<<#=typeName#>> items,
            Func<<#=typeName#>, IEnumerable<<#=typeName#>>> function)
        {
            foreach(var item in items)
            {
                foreach(var itemChild in function(item))
                {
                    yield return itemChild;
                }
            }
        }

        /// <summary>
        /// Applies the given function to each of the items in the supplied
        /// IEnumerable, which match the given type.
        /// </summary>
        public static IEnumerable<<#=typeName#>> 
            DrillDown<T>(this IEnumerable<<#=typeName#>> items,
            Func<<#=typeName#>, IEnumerable<<#=typeName#>>> function)
            where T : <#=typeName#>
        {
            foreach(var item in items)
            {
                foreach(var itemChild in function(item))
                {
                    if (itemChild is T)
                    {
                        yield return (T)itemChild;
                    }
                }
            }
        }
    
        /// <summary>
        /// Returns a collection of descendant elements.
        /// </summary>
        public static IEnumerable<<#=typeName#>> 
               Descendants(this IEnumerable<<#=typeName#>> items)
        {
            return items.DrillDown(i => i.Descendants());
        }    
       
        /// <summary>
        /// Returns a collection of descendant elements which match the given type.
        /// </summary>
        public static IEnumerable<<#=typeName#>> 
               Descendants<T>(this IEnumerable<<#=typeName#>> items)
            where T : <#=typeName#>
        {
            return items.DrillDown<T>(i => i.Descendants());
        }

    }
}<#+
}
#>

模板中的代码与前面各节中的代码非常相似。主要区别在于ILinqToTree适配器的使用现在被限定在每个扩展方法内,以便LINQ to Tree API直接作用于类型本身。这消除了对“常规”方法上的泛型类型参数的需求,允许我们使用单个类型参数来搜索集合以查找特定类型。

LINQ to VisualTree

为了生成LINQ to Visual Tree API,我们首先创建一个ILinqTree实现:

/// <summary>
/// Adapts a DependencyObject to provide methods required for generate
/// a Linq To Tree API
/// </summary>
public class VisualTreeAdapter : ILinqTree<DependencyObject>
{
    private DependencyObject _item;

    public VisualTreeAdapter(DependencyObject item)
    {
        _item = item;
    }

    public IEnumerable<DependencyObject> Children()
    {
        int childrenCount = VisualTreeHelper.GetChildrenCount(_item);
        for (int i = 0; i < childrenCount; i++)
        {
            yield return VisualTreeHelper.GetChild(_item, i);
        }
    }

    public DependencyObject Parent
    {
        get
        {
            return VisualTreeHelper.GetParent(_item);
        }
    }
}

然后,执行T4模板,使用所需的类型:

<#@ template language="C#v3.5" #>
<#@ include file="..\LinqToTreeCodeGen.tt" #>
<#
string typeName = "System.Windows.DependencyObject";
string adapterType = "LinqToVisualTree.VisualTreeAdapter";
string targetNamespace = "LinqToVisualTree";
GenerateLinqMethods(typeName, adapterType, targetNamespace);

#>

通过这几行代码,T4模板生成了一个约400行的API,它允许您查询树,利用前面描述的所有XPath轴。例如,生成的Descendants方法如下所示:

/// <summary>
/// Returns a collection of descendant elements.
/// </summary>
public static IEnumerable<DependencyObject> Descendants(this DependencyObject item)
{
    ILinqTree<DependencyObject> adapter = new VisualTreeAdapter(item);
    foreach (var child in adapter.Children())
    {
        yield return child;

        foreach (var grandChild in child.Descendants())
        {
            yield return grandChild;
        }
    }
}

我将不会在这篇文章中重现整个LINQ to Visual Tree API,代码本身真的不那么有趣!(您可以在本文附带的代码中找到完整的API。)更有趣的是,我们现在拥有一个通用的LINQ树结构API,只需几行代码即可针对特定树类型生成API。在探索其在其他一些树状结构中的应用之前,让我们回顾一下我们之前应用于可视化树的查询。使用新API,查询更加简洁:

// get all the TextBox's which have a Grid as direct parent
var itemsFluent = this.Descendants<TextBox>()
                      .Where(i => i.Ancestors().FirstOrDefault() is Grid);

var itemsQuery = from v in this.Descendants<TextBox>()
                 where v.Ancestors().FirstOrDefault() is Grid
                 select v;

// get all the StackPanels that are within another StackPanel visual tree
var items2Fluent = this.Descendants<StackPanel>()
                       .Descendants<StackPanel>();

var items2Query = from i in
                     (from v in this.Descendants<StackPanel>()
                      select v).Descendants<StackPanel>()
                  select i;

LINQ to Windows Forms

另一个常见的树结构是Windows Forms应用程序中的分层控件结构。这个结构对于WinForms开发人员来说并不总是那么明显,因为它不像WPF/Silverlight中的可视化树结构那样重要,在WPF/Silverlight中,属性继承由树结构支持。然而,您可以通过切换到“文档大纲”视图看到UI确实是一个树:

DocumentView.png

创建我们的LINQ to Windows Forms API同样只是创建一个ILinqToTree适配器来处理Control

/// <summary>
/// Adapts a Control to provide methods required for generate
/// a Linq To Tree API
/// </summary>
class WindowsFormsTreeAdapter : ILinqTree
{
    private Control _item;

    public WindowsFormsTreeAdapter(Control item)
    {
        _item = item;
    }

    public IEnumerable Children()
    {
        foreach (var item in _item.Controls)
        {
            yield return (Control)item;
        }
    }

    public Control Parent
    {
        get
        {
            return _item.Parent;
        }
    }
}

并且,创建利用我们前面描述的API生成模板的T4模板:

<#@ template language="C#v3.5" #>
<#@ include file="..\LinqToTreeCodeGen.tt" #>
<#
string typeName = "System.Windows.Forms.Control";
string adapterType = "LinqToWindowsForms.WindowsFormsTreeAdapter";
string targetNamespace = "LinqToWindowsForms";

GenerateLinqMethods(typeName, adapterType, targetNamespace);
#>

这里有两个简单的示例,使用了这个生成的API:

// find any buttons that are within GroupBoxes
var buttons = this.Descendants<Button>()
                    .Where(i => i.Ancestors<GroupBox>().Any());

// find all visible textboxes
var textBox = this.Descendants<TextBox>()
                  .Cast<TextBox>()
                  .Where(t => t.Visible);

LINQ to Filesystem

我想您现在应该知道怎么做了……

以下是适配DirectoryInfo类型的类的ILinqToTree实现:

/// <summary>
/// Adapts a DirectoryInfo to provide methods required for generate
/// a Linq To Tree API
/// </summary>
class FileSystemTreeAdapter : ILinqTree<DirectoryInfo>
{
    private DirectoryInfo _dir;

    public FileSystemTreeAdapter(DirectoryInfo dir)
    {
        _dir = dir;                   
    }

    public IEnumerable<DirectoryInfo> Children()
    {
        DirectoryInfo[] dirs = null;
        try
        {
            dirs = _dir.GetDirectories();
        }
        catch (Exception)
        { }

        if (dirs == null)
        {
            yield break;
        }
        else
        {
            foreach (var item in dirs)
                yield return item;
        }
    }

    public DirectoryInfo Parent
    {
        get
        {
            return _dir.Parent;
        }
    }
}

我还向DirectoryInfo添加了另一个扩展方法来适配返回数组的“GetFiles”方法,使其更易于LINQ使用。对于熟悉XPath的各位来说,在这个上下文中,文件可以看作是XML文档中的属性,并构成另一个可以查询的轴。

public static class DirectoryInfoExtensions
{
    /// <summary>
    /// An enumeration of files within a directory.
    /// </summary>
    /// <param name="dir"></param>
    /// <returns></returns>
    public static IEnumerable<FileInfo> Files(this DirectoryInfo dir)
    {            
        return dir.GetFiles().AsEnumerable();
    }
}

这里有几个使用此API的示例:

我个人在硬盘上有很多Visual Studio项目,它们是我许多想法的成果,有些好有些坏,但都缺乏组织。我倾向于将较好的项目保存在Subversion (SVN) 中。以下查询查找了我添加到SVN的所有项目:

var dir = new DirectoryInfo(@"C:\Projects");

// find all directories that contain projects checked into SVN
var dirs = dir.Descendants()
      .Where(d => d.Elements().Any(i => i.Name == ".svn"))
      .Where(d => !d.Ancestors().FirstOrDefault().Elements().Any(i => i.Name == ".svn"));

foreach (var d in dirs)
{
    Debug.WriteLine(d.FullName);
}

查询中的第一个where子句查找包含“.svn”子目录的任何目录。第二个where子句匹配那些其直接父目录本身不包含“.svn”目录的目录(当您将项目添加到SVN时,所有文件夹都会添加一个.svn文件夹)。

这是另一个有趣的示例,它提供了查询和流畅语法有趣的混合:

// find all 'bin' directories that contain XML files, and output
// the number of XML files they contain.
var dirsWithXML = 
    from d in dir.Descendants().Where(d => d.Name == "bin").Descendants()
        let xmlCount = d.Files().Where(i => i.Extension == ".xml").Count()
        where xmlCount > 0
        select new{ Directory = d, XmlCount = xmlCount};
    

foreach (var d in dirsWithXML)
{
    Debug.WriteLine(d.Directory + " [" + d.XmlCount + "]");
}

查询的第一部分,“in”子句,查找所有bin目录并选择它们的所有后代目录;“let”子句将每个目录中XML文件的数量赋值给变量“xmlCount”;where子句选择计数大于0的目录;最后,selects子句创建一个匿名类型,包含目录和XML文件数量。输出看起来像这样:

...
C:\Projects\SLF\Source\Facades\SLF.BitFactoryFacade\bin\Debug [1]
C:\Projects\SLF\Source\Facades\SLF.BitFactoryFacade\bin\Release [3]
C:\Projects\SLF\Source\Facades\SLF.EntLib20Facade\bin\Debug [3]
C:\Projects\SLF\Source\Facades\SLF.EntLib20Facade\bin\Release [4]
C:\Projects\SLF\Source\Facades\SLF.EntLib41Facade\bin\Debug [2]
C:\Projects\SLF\Source\Facades\SLF.EntLib41Facade\bin\Release [3]
...

结论和最后一个示例

我将用最后一个例子来结束这篇文章,纯粹为了好玩。回到前面LINQ to VisualTree实现中使用的示例XAML。以下单行查询:

string tree = this.DescendantsAndSelf().Aggregate("",
    (bc, n) => bc + n.Ancestors().Aggregate("", 
      (ac, m) => (m.ElementsAfterSelf().Any() ? "| " : "  ") + ac,
    ac => ac + (n.ElementsAfterSelf().Any() ? "+-" : "\\-")) + 
      n.GetType().Name + "\n");

输出树结构的ASCII表示:

\-Window1
  \-Border
    \-AdornerDecorator
      +-ContentPresenter
      | \-Grid
      |   +-TextBox
      |   | \-ClassicBorderDecorator
      |   |   \-ScrollViewer
      |   |     \-Grid
      |   |       +-Rectangle
      |   |       +-ScrollContentPresenter
      |   |       | +-TextBoxView
      |   |       | | \-DrawingVisual
      |   |       | \-AdornerLayer
      |   |       +-ScrollBar
      |   |       \-ScrollBar
      |   \-StackPanel
      |     +-TextBox
      |     | \-ClassicBorderDecorator
      |     |   \-ScrollViewer
      |     |     \-Grid
      |     |       +-Rectangle
      |     |       +-ScrollContentPresenter
      |     |       | +-TextBoxView
      |     |       | | \-DrawingVisual
      |     |       | \-AdornerLayer
      |     |       +-ScrollBar
      |     |       \-ScrollBar
      |     +-Grid
      |     | +-TextBox
      |     | | \-ClassicBorderDecorator
      |     | |   \-ScrollViewer
      |     | |     \-Grid
      |     | |       +-Rectangle
      |     | |       +-ScrollContentPresenter
      |     | |       | +-TextBoxView
      |     | |       | | \-DrawingVisual
      |     | |       | \-AdornerLayer
      |     | |       +-ScrollBar
      |     | |       \-ScrollBar
      |     | \-StackPanel
      |     |   +-TextBox
      |     |   | \-ClassicBorderDecorator
      |     |   |   \-ScrollViewer
      |     |   |     \-Grid
      |     |   |       +-Rectangle
      |     |   |       +-ScrollContentPresenter
      |     |   |       | +-TextBoxView
      |     |   |       | | \-DrawingVisual
      |     |   |       | \-AdornerLayer
      |     |   |       +-ScrollBar
      |     |   |       \-ScrollBar
      |     |   \-RadioButton
      |     |     \-BulletDecorator
      |     |       +-ClassicBorderDecorator
      |     |       | \-Ellipse
      |     |       \-ContentPresenter
      |     |         \-TextBlock
      |     \-ListBox
      |       \-ClassicBorderDecorator
      |         \-ScrollViewer
      |           \-Grid
      |             +-Rectangle
      |             +-ScrollContentPresenter
      |             | +-ItemsPresenter
      |             | | \-VirtualizingStackPanel
      |             | |   +-ListBoxItem
      |             | |   | \-Border
      |             | |   |   \-ContentPresenter
      |             | |   |     \-TextBlock
      |             | |   \-ListBoxItem
      |             | |     \-Border
      |             | |       \-ContentPresenter
      |             | |         \-TextBlock
      |             | \-AdornerLayer
      |             +-ScrollBar
      |             \-ScrollBar
      \-AdornerLayer

当然,同样的查询可以应用于本文中生成的任何LINQ to Tree API。

总之,本文通过实现一个简单的适配器接口和使用T4模板进行代码生成,演示了如何为任何树状结构生成LINQ API。希望您觉得这篇文章很有趣,并可能学到了一些关于LINQ to XML API或XPath轴的新知识。

如果您能将此技术应用于本文未涵盖的其他树结构,我很乐意听您分享,请在下方留言。

尽情享用!

历史

  • 2010年3月3日
    • 文章更新,ILinqToTree 接口基于William Kempf的反馈进行了修改
  • 2010年3月2日
    • 文章首次发布
© . All rights reserved.