SharePoint 2010开发最佳实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.2 使用LINQ操作列表

LINQ是Language Integrated Query的缩写,于2007年正式作为.NET 3.5的一部分而发布。LINQ的提出是为数据访问提供共通的接口。在没有LINQ之前,想想我们要访问数据库、XML文件、数组等都要通过不同的方式进行数据的访问。而LINQ将为不同数据源的访问提供统一的方式,降低了开发人员的学习成本。此外,LINQ还引入了很多新的特性,比如Lambda表达式、匿名类型等,这一节首先介绍LINQ的特性,并进一步介绍其在SharePoint里的使用。

3.2.1 LINQ语法概述

如前所述,LINQ的目的是为了为不同数据源的访问提供统一的查询语言,随着LINQ的发展,现在已经有若干个不同版本的LINQ,其中System.Linq提供了核心的功能,但针对不同的数据可以选择其他版本的LINQ来覆盖System.Linq,比如访问SQL数据时可以引用System.Data.Linq,访问XML数据时可以引用System.Xml.Linq,访问SharePoint列表时可以引用Microsoft.SharePoint.Linq。不管使用哪个版本的LINQ,目的都是要做到使用统一的方式来访问数据源。

下面是一段简单的LINQ代码,循环输出字符串数组里的所有字符串信息。这是一个Windows Console程序,注意在使用前使用using Sytem.Linq引入对LINQ类库的引用。

string[] names={"Ake","ShuangEr","ZengRou","JianNingGongZhu","SuQuan","MuJianPing",
"FangYi"};
var q=from n in names
      orderby n
      select n;
Console.WriteLine("The seven wives of WeiXiaoBao:");
foreach (string name in q){
    Console.WriteLine(name);
}

理解LINQ语法元素

为了支持如上例中的语法,C#语言做了很多改良,这些改良支持简化并统一了查询的语法。下面是几个重要的改良点,理解这些将帮助开发人员深入理解LINQ的语法。

泛型集合

泛型集合对于LINQ管理数据并提供强类型对象集合支持非常重要。泛型集合在.NET 2.0开始被引入,包含在System.Collection.Generic命名空间内。使用泛型,可以创建容纳特定对象的列表、字典、堆栈、队列、链接表等。下面的例子显示了仅包含学生对象的列表:

List<Student> students=new List<Student>

泛型集合比传统的集合(比如数组等)拥有更明显的优势,因为它们是类型安全的。这一位置泛型集合只接受特定的对象,也意味着和泛型结合相关的代码可以在编译时进行检查,而不是在运行的时候才能检查。此外,泛型集合还实现了System.Collections.IEnumerable接口从而支持枚举,可以通过foreach对数据进行循环查询,这点对LINQ来说非常重要。

扩展方法

扩展方法也是.NET的一个新特性,它支持在不继承类的情况下扩展类的方法,比如我们想为string类创建一个叫做IsValidEmailAddress的方法,用来校验字符串是否是合法的邮箱地址。要创建扩展方法,首先要创建一个静态类,然后在此静态类内创建静态的扩展方法,下例展示了如何创建的方法:

class Program{
    static void Main(string[] args){
        //扩展方法
        string email="test@test.com";
        Console.WriteLine(email.IsValidEmailAddress());
    }
}
public static class ExtensionMethod{
    public static bool IsValidEmailAddress(this string s){
        Regex r=new Regex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$");
        return r.IsMatch(s);
    }
}

一旦扩展方法创建好以后,就可以供相应的类调用了,Visual Studio对此也提供了相应的智能支持。如图3-5所示,创建好扩展方法之后在string类的方法列表里很快就能看到新创建的方法IsValidEmailAddress()了。

图3-5 Visual Studio提供的智能支持

扩展方法对LINQ查询非常重要,LINQ的很多查询功能就是通过扩展而来的。System.Linq命名空间下很多方法都扩展了IEnumerable接口。如果我们不应用System.Linq,使用IEnumerable会发现其下只有很简单的几个方法,如图3-6所示。

图3-6 没有引入System.Linq的 IEnumerable

但在引入System.Linq之后会发现方法增加了很多,如图3-7所示。

图3-7 引入了System.Linq的IEnumerable

增加的很多方法比如First()、Last()等将为数据的查询带来很多方便,而这其中最重要的恐怕就是Where方法了,这个方法可以帮助通过查询来返回所需要的数据。下面的代码通过代理方法的方式查询字符串数组,并按照条件返回符合查询条件的内容:

private static void DelegateLINQ(){
    string[] names={ "Ake","ShuangEr","ZengRou","JianNingGongZhu","SuQuan","MuJianPing",
    "FangYi" };
        foreach (string name in names.Where(new Func<string,bool>(FirstWife))){ Console.WriteLine("First wife of WeiXiaoBao is "+name);
    }
}
private static bool FirstWife(string name){ return name=="ShuangEr";
}

如果调用DelegateLINQ()方法会在控制台输出“First wife of WeiXiaoBao is: ShuangEr”,大家可能会留意到这段代码和本节最早展示的LINQ代码不一样,其中最大的差别是之前的LINQ代码使用的是查询语法,而此处的代码使用的是Where方法。Where方法和传统的代码比较像,而查询语法则比较类似于SQL的查询,上述代码也可以转换成如下查询方式的代码:

string[] names={ "Ake","ShuangEr","ZengRou","JianNingGongZhu","SuQuan","MuJianPing",
"FangYi" };
var q=from n in names
      where n=="ShuangEr"
      orderby n
      select n;
foreach(string name in q){
    Console.WriteLine("First wife of WeiXiaoBao is "+name);
}

以上代码执行后也可以获得同样的输出:“First wife of WeiXiaoBao is ShuangEr”。

理解Lambda表达式

代理方法的方式进行LINQ查询比较麻烦,需要声明、定义还要作为一个参数传给Where方法,在C#里还可以通过匿名的方式减少代理:

string[] names={ "Ake","ShuangEr","ZengRou","JianNingGongZhu","SuQuan","MuJianPing",
"FangYi" };
foreach (string name in names.Where(delegate(string s){ return s=="SuQuan"; })){
    Console.WriteLine("Oldest wife of WeiXiaoBao is: "+name);
}

尽管Delegate缩短了开发LINQ所需要的代码行数,但是C#做得更彻底,它引入了Lambda表达式,Lambda表达式使用Lambda操作符“=>”,可以理解成“转到”的意思。左边的内容为输入参数(如果有的话),右边的为声明或表达模块,上面的代码也可以用Lambda重写如下:

string[] names={ "Ake","ShuangEr","ZengRou","JianNingGongZhu","SuQuan","MuJianPing",
"FangYi" };
foreach (string name in names.Where(s=> s=="Ake")){
    Console.WriteLine("Most pretty wife of WeiXiaoBao is "+name);
}

执行上述代码将输出:“Most pretty wife of WeiXiaoBao is Ake”。

理解预测(Projections)

在属性、对象还有集合上,C#还做了很多其他方面的改进,比如自动属性、快捷初始化器还有匿名类型。这些改进在LINQ中都有体现。

自动属性功能允许定义类的属性而不需要设置其成员变量。如果读取(getter)设置(setter)器里没有逻辑代码的时候,自动属性会非常有用,下面的代码显示了类里自动属性的一个简单例子:

public class Student{
    public string FirstName{get; set;}
    public string LastName{get; set;}
    public int Age{get; set;}
}

C#还提供了对象和集合初始化的快捷方式,这些快捷方式帮助以简洁的方式创建对象和整个集合:

//对象初始化
Student s=new Student{FirstName="QiGong",LastName="Hong",Age=60};
//集合初始化
List<Student> students=List<Student>{
    Student s=new Student{FirstName="QiGong",LastName="Hong",Age=60};
    Student s=new Student{FirstName="YangFeng",LastName="Ou",Age=58};
    Student s=new Student{FirstName="LaoXie",LastName="Huang",Age=63};
}

与自动属性和快捷初始化联系紧密的还有匿名类型,匿名类型允许不定义类型而创建只读属性,编译器将自动创建类型,此类型通过关键字var来引用。此种对象仍然是强类型对象,但类型名不开放给代码。下面的例子显示了一个简单的匿名类型快捷实例化:

var stu=new {FirstName="LaoXie",LastName="Huang",Age=63};
Console.WriteLine(stu.FirstName);

3.2.2 在SharePoint中使用LINQ

我们在前面提到了在SharePoint中使用LINQ的原因之一是因为CAML语法难以编辑,无法在编译的时候发现错误,而且出错后很难获得有用的调试信息。还有很多其他原因,比如LINQ的查询语法更具有可读性,而且还提供了对数据的更新、删除等操作。但同时也要注意到,本质上讲LINQ编写的代码最终会被翻译成CAML执行。LINQ应该作为SharePoint列表数据访问的优先方式,除非需要去覆盖Throttling设置或者需要进行大范围数据查询(SPSiteDataQuery在LINQ里没有替代方式)。

在SharePoint中使用LINQ要引用命名空间Microsoft.SharePoint.Linq,我们要做的第一步是通过SPMetal工具生成实体类。

1.使用SPMetal工具

SharePoint列表数据维护在内容数据库里,这意味着列表和列表项的数据结构是建立在关系数据库的基础之上的。但是作为SharePoint开发人员,不需要了解这些数据表的结构,因为对象模型已经将这些结构抽象成SPList和SPListItem对象。开发LINQ代码访问SharePoint数据也是一样,我们不需要了解内容数据库的表结构,可以像使用对象模型一样来开发LINQ。

SharePoint LINQ在内容数据库之上建立一个抽象对象层,可以通过实体类访问列表或者列表项。实体类是轻量级的面向对象接口,并且实体类能够跟踪变化并处理更新时的并发等。

实体类通过一个叫做SPMetal的工具创建,这个工具位于C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\BIN。使用SPMetal创建实体类非常容易,只需要指定需要创建实体类对象的站点URL,指定需要创建的实体类的名称就可以,下面的代码是一个很简单的创建实体类的例子:

SPMetal /web:http://localhost /code:C:/Home.cs

上面的代码将在C盘根目录下生成一个叫做Home的CS文件。表3-9是SPMetal的一些常用参数。

表3-9 SPMetal常用参数

如果检查通过SPMetal生成的代码,会发现生成了两种类型的类:一个是继承自Microsoft.SharePoint.Linq.DataContext,DataContext类提供了到列表的链接并提供了追踪数据变化操作的方法,可以把这个类理解成类似于SqlConnection;另外还生成了很多在各个列表了里使用到的内容类型实体类。通过DataContext类和实体类的结合就可以通过LINQ进行数据查询了。下面的代码展示了如何通过LINQ的方式查询列表数据:

using (HomeDataContext dataContext=new HomeDataContext("http://localhost")){
    var q=from e in dataContext.Employee
          orderby e.EmployeeFullName
          select e;
    foreach (var employee in q){
        Console.WriteLine(employee.EmployeeFullName);
    }
}

HomeDataContext是通过上面我们调用的SPMetal命令生成的DataContext子类,由于我们指定了文件名为Home.cs,因此SharePoint自动生成了名为HomeDataContext的子类。另外,如果需要执行此段代码,需要引用Microsoft.SharePoint.Linq命名空间,此命名空间位于C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI文件夹。

2.理解DataContext类

在进行任何LINQ操作之前,首先必须通过DataContext对象建立到站点的连接,DataContext类可以在构造函数里指定站点的URL,当然这里的URL必须和建立实体类时的URL向匹配,否则操作会失败。另外,DataContext类也实现了IDisposable接口。因此,可以结合using语法一起使用。

DataContext类提供了泛型的GetList<T>,提供了访问每一个列表的接口(这些列表都有生成相应的内容类型实体类),同时也提供了EntityList<T>属性访问每个列表,上面的代码中就是使用Entity<T>的方式对列表进行查询。

DataContext类的log属性用来存储LINQ语法背后的CAML语句,可以用来调试,当然也可以用来创建CAML语法以供SPQuery或者SPSiteDataQuery使用。log属性可以接受System.IO.TextWriter对象,因此可以将CAML内容输出到文件或者控制台等接口供查看,以下代码显示了如何通过控制台的方式查看LINQ查询背后的CAML:

try{
    using (HomeDataContext dataContext=new HomeDataContext("http://localhost")){
    StringBuilder stringBuilder=new StringBuilder();
    TextWriter writer=new StringWriter(stringBuilder); dataContext.Log=writer;
    var q=from e in dataContext.Employee
          orderby e.EmployeeFullName
          select e;
    foreach (var employee in q){
        Console.WriteLine(employee.EmployeeFullName);
    }
    string queryLog=stringBuilder.ToString();
        Console.WriteLine(queryLog);
    }
}
catch (Exception ex){
    Console.WriteLine(ex.StackTrace);
}

DataContext类会跟踪实体类属性的变化,然后可以将这些变化写回内容数据库。ObjectTrackingEnabled属性设置决定DataContext类是否会跟踪实体类的变化,这个属性的默认设置为True。将其设置为False,将使相应的DataContext类只读,并因此而带来性能上的改善。如果DataContext类跟踪变化,则可以通过调用其SubmitChanges方法将变化提交到内容数据库,稍后我们将做详细介绍。

3.在Code生成中使用Parameters.xml

前面介绍SPMetal时列举了能够在命令行调用时使用的一些参数,比如/web等,如果想更加严格地控制代码的生成,可以使用Parameters.xml作为参数生成代码,而Parameters.xml文件支持更丰富的参数控制。

Parameters.xml文件为SPMetal命令提供生成代码的详细条件,特别是在这个XML文件里可以指定哪些列表、内容类型、字段需要在代码里生成。这个XML文件作为参数通过/parameters<文件名>传递给SPMetal命令,下面是一个Parameters.xml的例子:

<?xml version="1.0" encoding="utf-8"?>
<Web Class="Entities" AccessModifier="Public"
xmlns="http://schemas.microsoft.com/SharePoint/2009/spmetal">
<List Name="Employee" Member="Employee">
  <ContentType Name="Item" Class="Employee">
    <Column Name="EmployeeFullName" />
    <ExcludeOtherColumns/>
  </ContentType>
</List>
<ExcludeOtherLists/>
</Web>

上面的示例代码里Web元素是根元素,Class属性指定了生成的DataContext子类的名字,AccessModifier属性指定了生成类的访问级别。List元素是Web元素的一个子元素,指定了需要生成哪个列表的实体类,Name属性指定了列表的名称,Member属性指定了在DataContext类中代表该列表的名称。ContentType元素是列表元素的子元素,指定了需要生成实体类的内容类型,Name属性指定了该内容类型的名称,Class属性指定了将要生成的类的名称,Column属性指定了要包含的字段名称,ExcludeOtherColumns、ExcludeOtherContentTypes、ExcludeOtherLists元素指定了是否需要将其他的字段、内容类型或者列表包含进来,如果不指定此元素,默认会将所有其他相应类型包含进来。这将对开发非常有帮助,能够将不使用的类、字段、内容类型等排除在实体类之外。表3-10是parameters.xml支持的所有元素的列表。

表3-10 Parameters.xml支持元素列表

4.使用LINQ查询SharePoint

生成实体类之后就可以开始书写LINQ进行SharePoint数据查询了。使用LINQ语法查询SharePoint数据和查询其他数据源非常类似。首先,通过查询语法进行数据查询,结果返回到匿名类型,然后通过IEnumerable接口来进行数据的迭代。

LINQ也支持对通过Lookup连接起来的列表进行关联列表查询,之前我们介绍过如何通过CAML进行这种查询,LINQ实现这种查询与CAML相比要简单得多。我们先回忆一下这个例子的背景,我们首先建有一个员工列表(Employee)包括员工ID(EmployeeID)、员工全名(EmployeeFullName)、员工部门(Employee Department)信息,之后我们建立了一个薪酬列表(Salary),包含员工ID(Employee)、薪酬(Salary)等信息,需要实现的功能是通过跨表查询将薪酬大于60 000的员工的全名和薪酬输出到控制台窗口。下面我们通过LINQ来实现通过CAML代码实现的同样功能。

try{
    using (HomeDataContext dataContext=new HomeDataContext("http://localhost")){
        var q=from s in dataContext.Salary
              where s.Salary > 60000.00
              select new {FullName=s.Employee.EmployeeFullName,s.Salary };
        foreach (var employee in q){
            Console.WriteLine(employee.FullName+": "+employee.Salary);
        }
    }
}
catch (Exception ex){
    Console.WriteLine(ex.StackTrace);
}

上述代码通过s.Employee.EmployeeFullName简单地就实现了与薪酬列表项关联的EmployeeFullName的查询。注意,我们使用了“FullName=s.Employee.EmployeeFullName”,此处使用了匿名对象,使用FullName来代替EmployeeFullName,这样我们在输出时可以通过employee.FullName直接调用此值。而Visual Studio的智能识别功能会在我们输入employee.之后自动识别到新的匿名属性FullName。

LINQ对SharePoint的查询还支持组合查询,组合查询使得一个LINQ查询可以在另一个LINQ查询的基础上进一步查询。如下代码实现了和前面代码同样的功能:

try{
    using (HomeDataContext dataContext=new HomeDataContext("http://localhost")){
        var q1=from s1 in dataContext.Salary
               select new { FullName=s1.Employee.EmployeeFullName,s1.Salary };
        var q2=from s2 in q1
               where s2.Salary > 60000.00
               select s2;
        foreach (var employee in q2){
            Console.WriteLine(employee.FullName+": "+employee.Salary);
        }
    }
}
catch (Exception ex){
    Console.WriteLine(ex.StackTrace);
}

最后LINQ对SharePoint数据的查询支持一系列的扩展方法,可以用来聚合、分组并返回特定的实体,这些方法经常被用在查询返回的结果上。下面的代码显示了如何获得查询返回结果的条数:

var q=(from s in dataContext.Salary
        select s).Count();
Console.WriteLine(q);

表3-11列出了常用的扩展方法。

表3-11 LINQ常见扩展方法

5.使用LINQ增加、删除、更新SharePoint

除了数据查询,还可以进行列表的添加、删除和更新。通过Entity<T>属性关联的方法可以添加或者删除列表项。InsertOnSubmit方法插入一条新数据到列表中去,InsertAllOnSubmit方法添加包含若干条新数据的集合到列表中去。DeleteOnSubmit方法从列表中删除一条数据,DeleteAllOnSubmit方法删除包含若干条数据的集合。RecycleOnSubmit方法删除一条数据到回收站,RecycleAllOnSubmit方法删除包含若干条数据的集合到回收站。下面的代码展示了如何插入一条数据到员工列表。数据修改后要记得调用DataContext.SubmitChanges()提交数据修改到内容数据库。

try{
    using (HomeDataContext dataContext=new HomeDataContext("http://localhost")){
        EmployeeItem newEmployee=new EmployeeItem(); newEmployee.Title="00004";
        newEmployee.EmployeeFullName="HuangLaoXie";
        newEmployee.EmployeeDepartment="TaoHuaDao";
        dataContext.Employee.InsertOnSubmit(newEmployee);
        dataContext.SubmitChanges();
    }
}
catch (Exception ex){
    Console.WriteLine(ex.StackTrace);
}

更新列表项内容也非常类似,先查询获得相关的列表项,然后修改列表项的相关属性,最后调用DataContext.SubmitChanges()提交数据更新到内容数据库。下面的代码将修改上面代码中我们新添加的员工数据:

try{
    using (HomeDataContext dataContext=new HomeDataContext("http://localhost")){
        var q=(from s in dataContext.Employee
                where s.Title=="00004"
                select s).First();
        q.EmployeeDepartment="ZhongNanShan";
        dataContext.SubmitChanges();
    }
}
catch (Exception ex){
    Console.WriteLine(ex.StackTrace);
}

当通过LINQ进行数据更新时,LINQ对并发进行了优化。在提交更新时LINQ会先查看被更新的列表项是否已经有更新(如用户通过浏览器画面更新了该列表项),如果有更新,则通过此LINQ更新的数据不会被提交。

当在更新时发现了此种并发冲突时,LINQ会抛出Microsoft.SharePoint.Linq. ChangeConflictException异常。不仅如此,DataContext的ChangeConflicts集合将会被填充进ObjectChangeConfilct对象,此对象包含列表项中引起数据冲突的字段的相关信息。另外DataContext的SubmitChanges重载方法允许指定冲突的处理模式,在第一次冲突之后是应该继续尝试更新还是停止更新,ChangeConflicts集合会记录失败尝试的相关信息。

此处需要注意的是,如果选择第一次冲突之后就停止尝试,则ChangeConflicts集合将只能够记录第一处冲突的信息,选择继续尝试更新的目的就是为了让ChangeConflicts集合里能够包含所有的冲突信息。

ChangeConflicts集合包含一个MemberConflicts集合,包含详细的引起冲突的值的信息。特别是MemberConflict集合会被填充MemberChangeConflict对象,每个这样的对象里都会包含OriginalValue、CurrentValue和DatabaseValue属性。OriginalValue包含了LINQ查询返回的数据,CurrentValue是我们更新了准备提交给内容数据库的数据,DatabaseValue是我们提交数据时数据库里的值。

捕获ChangeConflictException异常,使用MemberChangeConflict对象可以将冲突的详细情况显示给用户,下面的代码显示了怎样处理此对象:

HomeDataContext dataContext=null;
try{
    dataContext=new HomeDataContext("http://localhost");
    //Update item
}
catch (ChangeConflictException ex){
    foreach (ObjectChangeConflict cc in dataContext.ChangeConflicts){
        foreach (MemberChangeConflict mc in cc.MemberConflicts){
            Console.WriteLine("Original Value: "+mc.OriginalValue);
            Console.WriteLine("Current Value: "+mc.CurrentValue);
            Console.WriteLine("Database Value: "+mc.DatabaseValue);
        }
    }
}
finally{
    if (dataContext !=null){
        dataContext.Dispose();
    }
}

除了显示冲突,我们还可以解决冲突,我们可以将冲突的信息显示在Web部件里,然后让用户选择希望使用哪个值,MemberChangeConflict的Resovle方法接受Microsoft.SharePoint.Linq.RefreshMode迭代,有三个值可供选择:KeepChanges、KeepCurrentValues或者OverwriteCurrentValues。KeepChanges将会接受所有的更新,但会以当前用户的更新优先;KeepCurrentValues将会保存当前用户的更新并丢掉所有其他的更新;OverwriteCurrentValues将会丢掉当前用户的更新而保留数据库内的当前值。调用Resolve方法之后,同样必须调用DataContext.SubmitChanges()方法提交更新。

HomeDataContext dataContext=null;
try{
    dataContext=new HomeDataContext("http://localhost");
    //Update item
}
catch (ChangeConflictException ex){
    foreach (ObjectChangeConflict cc in dataContext.ChangeConflicts){
        foreach (MemberChangeConflict mc in cc.MemberConflicts){
            mc.Resolve(RefreshMode.KeepCurrentValues);
        }
    }
    dataContext.SubmitChanges();
}
finally{
    if (dataContext !=null){
        dataContext.Dispose();
    }
}