原文地址:http://www.silverlightshow.net/items/Working-with-collections-in-WCF-RIA-Services-part-two.aspx
这是本文的第二部分。
在第一部分中,我们讨论了两个相对简单的集合类型: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")); });
效果如图:
有一点要注意,一旦对集合进行分组操作,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(); });
将以上内容整合一下,我们最后得到了一个服务端分页、排序和分组的集合:
总结
WCF RIA Services SP1新增或增强了许多的集合类型。无论是更好的绑定选项还是服务端可分页、排序的集合都让其与MVVM的交互变得更加便捷。如果你在使用WCF RIA Services配合MVVM开发行业软件或商用程序,那么对这些新的集合类型的了解就显得十分必要了。