2013年12月23日星期一

[译]WCF RIA Services中的集合 - 紫色永恒 - 博客园

[译]WCF RIA Services中的集合(1)

本文分为两部分,本篇为第一部分。
介绍
今天,很多的商业应用是使用WCF RIA Services构成的(这并不奇怪,它确实是一个强大的高扩展性框架)。然而它对集合类型的支持可以变得更好。你经常会做这样的操作:获取实体(通常是一个Load Operation)在其完成事件中将其添加到一个ObservableCollection<T>中,现在你依然可以这样做。不过在WCF RIA Services的第一个SP中分别对一些之前就存在的集合类型进行了加强,同事也增加了一些新的集合类型。这些变化使你在使用WCF RIA Services配合MVVM模式时更得心应手。现在我们可用的集合可以自动跟踪你的DomainContext,你可以添加过滤、排序甚至分组条件,还提供了一个服务端可分页的DomainCollectionView。在这篇文章中,我们会一起探讨一下这些增强及新增的新集合类型以及看看它们在哪些场景中使用起来更给力。
本文中将会涉及WCF RIA Services中的四种集合类型:EntitySet(增强)、EntityList(新增)、CollectionView(增强)以及DomainCollectionView(新增)。你可以在这里下载源码
WCF RIA Services SP1包含在Visual Studio 2010 SP1中,点此下载
EntitySet<T>
EntitySet是WCF RIA Services应用程序中的一个基本集合类型—当我们建立ViewModel时会经常用到它。它是一个带有一些用来自定义返回类型选项的无序集合。因此当我们想查看通过DomainContext载入的某个类型的所有实体时,我们会经常使用它。
下图是一个示例程序,我将Books属性绑定到ListBox上。
clip_image002
看下面的代码:
/// <summary>
/// The Books property
/// </summary>
public EntitySet<Book> Books{
    get{
        return Context.Books;
    }
}
像你看到的那样,Books属性是一个Book类型的EntitySet集合,它只是Context.Books的简单引用。在构造函数中,我们载入10条数据:
public EntitySetViewModel() {

    InstantiateCommands();

    // load books
    Context.Load<Book>(Context.GetBooksQuery().Take(10));
}
当需要更多数据时,这样
LoadMoreBooks = new RelayCommand(() => {
    Context.Load<Book>(Context.GetBooksQuery());
});
当更多的Book类型的实体被添加到正确的Context.Books这个EntitySet时,这些新增的Books也被加载到ListBox中。换句话说,当Book数据被DomainContext载入时,它们也被同时添加到ContextBooks这个EntitySet中了。
EntitySet<T>:添加和移除数据
那么如何向EntitySet添加或移除数据呢?下面的代码展示如何添加数据:
AddBook = new RelayCommand(() => {
    Context.Books.Add(new Book() {
        Author = "Kevin Dockx"
        ,
        ASIN = "123456"
        ,
        Title = "Dummy book"
    });
});
然后是移除数据:
DeleteBook = new RelayCommand(() => {
    Context.Books.Remove(Context.Books.FirstOrDefault());
});
当你调用SubmitChanges()方法时,DomainService会将增加/删除的数据切实的反映为服务端的相应操作。
EnityList<T>
下一个要介绍的集合类型是:EntityList<T>,它是一个新增的集合类型,你可以在WCF RIA Services Toolkit中找到它,它位于Microsoft.Windows.Data.DomainServices命名空间下(源码中已经包含了这个程序集)。本质上说它是一个基于EntitySet的Observable集合。它的优点是允许我们取得通过DomainContext载入的特定类型实体数据的子集。定义一个EntityList<T>属性大概是如下的样子:
private EntityList<Book> _books;
public EntityList<Book> Books {
    get {
        if (this._books == null) {
            this._books = new EntityList<Book>(
                this.Context.Books);
        }
        return this._books;
    }
}
Entity List有一个Source属性,它定义了EntityList应该包含的实体:
public EntityListViewModel() {
    InstantiateCommands();

    // load books
    this.Books.Source = Context.Load<Book>(Context.GetBooksQuery().Take(10)).Entities;
}
通过这两段代码,你的EntityList就已经初始化并准备好了。
一旦你开始读取更多的Book类型实体,事情就变得有趣起来了:数据变化不会自动的反映到你的EntityList中。如果你想让EntityList对那些新增的Book作出反应,则需要设置它的Source属性:
LoadMoreBooks = new RelayCommand(() => {
    this.Books.Source = Context.Load<Book>(Context.GetBooksQuery()).Entities;
});
EntityList<T>:添加和移除数据
添加一条新的Book实体通常通过两种方式,直接添加到Context中或添加到EntityList本身。这两种方式需要使用不同的方法处理。可以看到我们示例中的代码:
AddBook = new RelayCommand(() => {
    Context.Books.Add(new Book() {
        Author = "Kevin Dockx"
        ,
        ASIN = "123456"
        ,
        Title = "Dummy book"
    });
});
一条新的Book被添加到Context中。不过EntityList不会自动的获知这些变化(上文说过,实现项的子集的变化跟踪需要通过设置Source属性,而非每次Context的载入变化),ListBox仍然显示EntityList的Source属性中的实体数据集合。
当如下代码执行时:
AddBookToEntityList = new RelayCommand(() => {
    Books.Add(new Book() {
        Author = "Kevin Dockx"
        ,
        ASIN = "123456"
        ,
        Title = "Dummy book"
    });
});
一条新的Book实体被添加到EntityList中,这将直接反映到我们的ListBox控件中。如果EntitySet中不存在这条Book实体,则它会被同时添加到其对应的EntitySet中。
而移除一条Book的行为又有一些不同:当你从Context的EntitySet中移除Book数据的时候,它会直接反映到EntityList中:
DeleteBook = new RelayCommand(() => {
    Context.Books.Remove(Context.Books.FirstOrDefault());
});
当你从EntityList中移除一条Book时,EntitySet中也会同时移除这条实体。
DeleteBookFromEntityList = new RelayCommand(() => {
    Books.Remove(Books.FirstOrDefault());
});
这是本文的第一部分,第二部分将会对更高级的集合类型进行介绍,他们是:ICollectionView和DomainCollectionView,敬请期待。

http://www.cnblogs.com/024hi/archive/2011/07/15/2107554.html

[译]WCF RIA Services中的集合(2)


这是本文的第二部分。
在第一部分中,我们讨论了两个相对简单的集合类型:EntitySet和EntityList。在本文中,我们将更进一步的了解其他两个更高级的类型:ICollectionView和DomainCollectionView。

ICollectionView
ICollectionView并不是一个新的接口,已经有大量的Silverlight控件对其进行了实现,如DataGrid。现在,我们可以直接在ViewModel中使用它。为了允许控件绑定到一个ICollectionView的实现(如我们熟悉的CollectionViewSource和PagedCollectionView),我们可以这样做:

private ICollectionView CreateView(IEnumerable source) {
    CollectionViewSource cvs = new CollectionViewSource();
    cvs.Source = source;
    return cvs.View;
}

private ICollectionView _books;
public ICollectionView Books {
    get {
        if (this._books == null) {
            this._books = CreateView(this.Context.Books);
            this._books.Filter = new Predicate<object>(BookCorrespondsToFilter);
        }
        return this._books;
    }
}
当载入Book数据时会自动反映到View中:
public CollectionViewViewModel() {
    InstantiateCommands();

    // load books
    Context.Load<Book>(Context.GetBooksQuery().Take(10));
}

ICollectionView:添加和移除数据
可以通过直接从Context添加和移除实体来完成,这些EntitySet的变化变化都会被CollectionViewSource跟踪到。
那么,这么做的意义到底是什么?目前为止我们还没看到这种方式与直接使用EntitySet的区别是吧?其实,ICollectionView的真正特别之处体现在可以添加过滤、排序和分组规则。
ICollectionView的过滤
ICollectionView定义了一个Predicate<object>类型的Filter属性,让我们略加修改我们的代码:
private ICollectionView _books;
public ICollectionView Books {
    get {
        if (this._books == null) {
            this._books = CreateView(this.Context.Books);
            this._books.Filter = new Predicate<object>(BookCorrespondsToFilter);
        }
        return this._books;
    }
}

public bool BookCorrespondsToFilter(object obj) {
    Book book = obj as Book;

    if (filterActive) {
        return book.Title.Contains("Silverlight");
    }

    return true;
}
BookCorrespondsToFilter方法执行时会检查每一个Book的Title属性是否包含“Silverlight”这个单词,如果不包含,则它不会被显示在View中。
当前代码提供的功能仅当你明确知道过滤时机时使用,然而大部分的应用程序具有要用户自己确定过滤时机的需求,那么我们再来进行一些改动:添加filterActive属性,当用户点击Add Filter时它被置为true。
public bool BookCorrespondsToFilter(object obj) {
    Book book = obj as Book;

    if (filterActive) {
        return book.Title.Contains("Silverlight");
    }

    return true;
}
然当我们点击按钮的时候,我们会发现界面并没有发生任何变化,为什么呢?
当我们针对过滤条件做出改变或Book实体发生变化时(如更改它的书名),ICollectionView的实现不回自动再次执行过滤:Filter方法仅在将实体添加到EntitySet时执行。这意味你不得不明确的通知它使用新的过滤条件重新检查已经载入的实体,我们可以通过调用ICollection的Refresh()方法实现:
Refresh = new RelayCommand(() => {
    Books.Refresh();
});
现在,View会被重新创建,这会让所有的Book实体被重新过滤。当然这仅当我们改变过滤条件或EntitySet发生改变时才是必要的。

ICollectionView的排序和分组
ICollectionView具有SortDescriptions和GroupDescription这两个有趣的属性,可以使用它们定义针对EntitySet的排序和分组规则。
排序操作可以通过点击绑定了ICollectionView的DataGrid列头实现,但当我们使用其他的一些诸如ListBox一类没有表头的控件时则需要通过代码的方式改变它们的排序规则:
AddSort = new RelayCommand(() => {
    Books.SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending));
});
对集合的分组操作类似:
AddGrouping = new RelayCommand(() => {
    Books.GroupDescriptions.Add(new PropertyGroupDescription("Author"));
});
效果如图:
clip_image002
有一点要注意,一旦对集合进行分组操作,UI虚拟化将会被关闭-所以在操作大量数据时要谨慎一些。当对大量数据进行分组时一般要和分页相配合。
当我们要进行排序、过滤和分组操作时,ICollection是一个非常好的选择。然而它只能作用于内存中的数据,这意味着所有的数据都必须载入到客户端。这适合大部分的应用场景。而其他的情况我们可以通过DomainCollectionView解决。

DomainCollectionView
有很多的企业级应用中会有成千上万甚至百万千万级的数据要进行排序、过滤和分组。面对这类场景,ICollectionView就不再适用了,原因上文已经说明。我们需要一个允许服务端排序、过滤、分组以及更重要的分页操作的集合。
这就是DomainCollectionView的职责,你可以在WCF RIA Services Toolkit中的Microsoft.Windows.Data.DomainServices程序集中找到它(该程序集已经包含在示例代码中)。使用DomainCollectionView需要我们进行相比其他集合更多的设置,不过一旦你掌握了这些设置你会发现它们依然十分简单。DomainCollection初始化时需要Source和Loader(默认是CollectionViewLoader)属性。
public DomainCollectionView<Book> Books
{
    get { 
        return this.view; 
    }
}
Source定义了用于View的源实体(任意的实现了IEnumerable的集合),典型的例子就是实现了INotifyCollectionChanged的集合。
this.source = new EntityList<Book>(Context.Books);
而Loader关注点在于数据的载入。当我们使用默认的CollectionViewLoader时需要同事传入两个回调:OnLoad和OnLoadCompleted,它们分别定义当数据必须被载入和载入操作完成时的发生的事件(当然如果你愿意的话,也可以用一个简单些的LoadOperation代替CollectionViewLoader)。
this.loader = new DomainCollectionViewLoader<Book>(
    this.OnLoadBooks,
    this.OnLoadBooksCompleted);
private LoadOperation<Book> OnLoadBooks()
{
    return this.Context.Load(this.query.SortPageAndCount(this.view));
}

private void OnLoadBooksCompleted(LoadOperation<Book> op)
{
    if (op.HasError)
    {
        op.MarkErrorAsHandled();
    }
    else if (!op.IsCanceled)
    {
        this.source.Source = op.Entities;
        if (op.TotalEntityCount != -1)
        {
            this.Books.SetTotalItemCount(op.TotalEntityCount);
        }
    }
}
正像你所看到的那样,我们在OnLoadeBooks中确认请求执行时包含了排序(SortDescription)和分页(仅当前页所需数据被载入)以及数据总数(DataPager需要用到)。
当Books被载入时,Source集合会被设置成载入的Book实体,并通过TotalEntityCount得到实体总数赋给TotalItemCount属性。
this.view = new DomainCollectionView<Book>(loader, source);
其余的部分用来进行载入的初始化(如设置页大小为5等):
using (this.view.DeferRefresh())
{
    this.view.PageSize = 5;
    this.view.MoveToFirstPage();
}
(使用DeferRefresh()的可以让view的刷新数据事件推迟到所有using包含的代码段执行后再执行)
其实,当我们进行分页、排序(以及其他的可能出发View刷新的操作)时,Loader都被执行并载入数据。一旦载入操作完成便会更新Source属性并通过通知机制让View获知更新同时响应变化。
(注:在WCF RIA Services Toolkit的April版本中,SortPageAndCount已经改成了SortAndPageBy)

DomainCollectionView:添加和移除数据
代码如下
AddBook = new RelayCommand(() =>
{
    // you can add books like this, but DCV is server side oriented: to get correct 
    // behaviour, you should add it to the Context and submit the changes, after which
    // the next query will fetch the book you just added.
    Book book = Books.AddNew() as Book;
    book.Author = "Kevin Dockx";
    book.ASIN = "123456";
    book.Title = "Silverlight for dummies";
});

DeleteBook = new RelayCommand(() =>
{
    // deleting an item can be done like this, but should be done directly on the context 
    // & submitted to the server
    Books.RemoveAt(0);
});
然后这样的操作会导致View的筛选、排序等规则不同步,毕竟DomainCollectionView在被设计工作在服务端的。
正确的添加和移除实体的方式是同时在服务端进行相应的操作,如:添加一个实体到Context中(或从Context中移除),调用SubmitChanges提交至服务端然后刷新你的DomainCollectionView。
DomainCollectionView:数据的过滤、排序及分组
接下来我们看一下如何对数据进行过滤。很简单,只要在EntityQuery后面增加相应的Where条件即可,比如:
AddFilter = new RelayCommand(() =>
{
    // filters in DCV should be done by adding a Where clause to the query, as DCV is mainly used for
    // server side logic

    this.query = Context.GetOrderedBooksQuery().Where(b => b.Title.Contains("Silverlight"));
    this.view.MoveToFirstPage();

});
接下来是排序和分组。当我们点击列头时,SortDesscription会被添加到Book集合中决定下次读取数据的排序策略并自动重新获取数据。
有一些应用程序中要求当排序规则变化后列表跳转到第一页。为对应这样的需求,我们则要像这样写一条event handler:
INotifyCollectionChanged notifyingSortDescriptions = (INotifyCollectionChanged)this.Books.SortDescriptions;

notifyingSortDescriptions.CollectionChanged += (sender, e) => {
    this.view.MoveToFirstPage();
};
像使用ICollectionView一样,我们也可以添加使用代码向DomainCollectionView中添加自定义的SortDescription
AddSort = new RelayCommand(() => {
    Books.SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending));
    Books.Refresh();
});
分组的方式类似:
AddGrouping = new RelayCommand(() => {
    Books.GroupDescriptions.Add(new PropertyGroupDescription("Author"));
    Books.Refresh();
});
将以上内容整合一下,我们最后得到了一个服务端分页、排序和分组的集合:
clip_image004
总结
WCF RIA Services SP1新增或增强了许多的集合类型。无论是更好的绑定选项还是服务端可分页、排序的集合都让其与MVVM的交互变得更加便捷。如果你在使用WCF RIA Services配合MVVM开发行业软件或商用程序,那么对这些新的集合类型的了解就显得十分必要了。
http://www.cnblogs.com/024hi/archive/2011/07/19/2110186.html