EntityList<T>/DomainCollectionView<T>/DomainCollectionViewLoader<T>都包含在Microsoft.Windows.Data.DomainServices这个dll里面,添加这个dll引用,就可以用这三个东西了。用这3个东西可以实现Server-Side Filtering, Paging, and Sorting(在ViewModel中通过调用WCF Ria Service实现服务器端分页、排序、过滤,而不要用Domain DataSource这个MVVM不友好的控件):
- EntityList<T> is an ObservableCollection backed by an EntitySet, its important to know that when an entity is removed from the list it also gets removed from associated EntitySet but it is not automatically added when a new entity is added to backed EntitySet.
- DomainCollectionView<T> is a ICollectionView implementation, so it exposes everything you need to Page/Sort your data and accepts a CollectionViewLoader and a IEnumerable<T> as constructor parameters.
- DomainCollectionViewLoader<T> is a CollectionViewLoader implementation that accepts the method to invoke to load data and another method to invoke when load operation completes.
具体背景:
- 源码下载:Server-Side Filtering, Paging, and Sorting Using the DomainCollectionView
- Introducing An MVVM-Friendly DomainDataSource: The DomainCollectionView
- DomainCollectionView Updates for Mix '11
最后实现的截图(过滤/搜索/分页/排序,都是服务器端按需加载):
看看是不是服务器端分页,用Fiddler看看的确是返回当前页数据:
用Fiddler看看分页的http请求是这样的:
用Fiddler看看过滤/搜索的http请求是这样的:
实现原理
看看源码实现,重点是WCF Ria Service的DomainService有个Count函数,正如我上文说的, 这个分页取得总数的函数对服务器分页来说是省不了的。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.ServiceModel.DomainServices.Hosting; using System.ServiceModel.DomainServices.Server; using System.Threading; namespace DomainCollectionViewSample.Web { [EnableClientAccess()] public class SampleDomainService : DomainService { private const int UiWait = 1000; // Overridden to support paging over POCO entities. // If you're extending a derived DomainService // implementation this is already done for you. protected override int Count(IQueryable query) { return query.Count(); } public IQueryable GetAllEntities() { return this.GetEntities(byte.MaxValue); } public IQueryable GetEntities(byte count) { this.WaitALittle(); List entities = new List (); for (byte i = 1; i < count; i++) { entities.Add(this.GetEntity(i)); } return entities.AsQueryable(); } public SampleEntity GetEntity(byte key) { SampleEntity entity = new SampleEntity(); entity.Key = key; entity.Byte = key; entity.UInt16 = key; entity.Int32 = key; entity.Single = key; entity.String = SampleDomainService.GetEntityString(key); entity.Guid = Guid.Parse("00000000-0000-0000-0000-" + entity.String); return entity; } private void WaitALittle() { if (SampleDomainService.UiWait > 0) { Thread.Sleep(SampleDomainService.UiWait); } } /// /// Returns a unique string for the specified ///. /// /// Returns a 12-character string of decending numbers. For instance, /// "000000054321" is returned for 5 and "111098765432" is returned for /// 11. /// /// The key to get the string for ///A unique string private static string GetEntityString(byte key) { string str = string.Empty; while ((str.Length < 12) && (key > 0)) { str += key--; } while (str.Length < 12) { str = "0" + str; } return str.Substring(0, 12); } } public class SampleEntity { [Key] public int Key { get; set; } public Byte Byte { get; set; } public UInt16 UInt16 { get; set; } public Int32 Int32 { get; set; } public Single Single { get; set; } public String String { get; set; } public Guid Guid { get; set; } } }
ViewModel部分,用来给DataGrid和DataPager绑定的对象是DomainCollectionView<T>,这个视图由DomainCollectionViewLoader<T>来加载,排序和分页内部通过EntityQuery<T>.SortAndPageBy来实现,这个有一定的局限性。
using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.ServiceModel.DomainServices.Client; using System.Windows.Input; using DomainCollectionViewSample.Web; using Microsoft.Windows.Data.DomainServices; namespace DomainCollectionViewSample { public class SampleViewModel : INotifyPropertyChanged { private readonly SampleDomainContext _context = new SampleDomainContext(); private readonly DomainCollectionView_view; private readonly DomainCollectionViewLoader _loader; private readonly EntityList _source; private readonly ICommand _searchCommand; private bool _canLoad = true; private string _searchText = string.Empty; public SampleViewModel() { this._source = new EntityList (this._context.SampleEntities); this._loader = new DomainCollectionViewLoader ( this.LoadSampleEntities, this.OnLoadSampleEntitiesCompleted); this._view = new DomainCollectionView (this._loader, this._source); #region DesignerProperties.IsInDesignTool // Swap out the loader for design-time scenarios if (DesignerProperties.IsInDesignTool) { DesignTimeLoader loader = new DesignTimeLoader(); this._view = new DomainCollectionView (loader, loader.Source); } #endregion this._searchCommand = new RelayCommand(this.OnSearch); // Go back to the first page when the sorting changes INotifyCollectionChanged notifyingSortDescriptions = (INotifyCollectionChanged)this.CollectionView.SortDescriptions; notifyingSortDescriptions.CollectionChanged += (sender, e) => this._view.MoveToFirstPage(); using (this.CollectionView.DeferRefresh()) { this._view.PageSize = 10; this._view.MoveToFirstPage(); } } #region View Properties public bool CanLoad { get { return this._canLoad; } private set { if (this._canLoad != value) { this._canLoad = value; this.RaisePropertyChanged("CanLoad"); } } } public ICollectionView CollectionView { get { return this._view; } } public string SearchText { get { return this._searchText; } set { if (this._searchText != value) { this._searchText = value; this.RaisePropertyChanged("SearchText"); } } } public ICommand SearchCommand { get { return this._searchCommand; } } #endregion private LoadOperation LoadSampleEntities() { this.CanLoad = false; EntityQuery query = this._context.GetAllEntitiesQuery(); if (!string.IsNullOrWhiteSpace(this.SearchText)) { query = query.Where(e => e.String.Contains(this.SearchText)); } return this._context.Load(query.SortAndPageBy(this._view)); } private void OnLoadSampleEntitiesCompleted(LoadOperation op) { this.CanLoad = true; if (op.HasError) { // TODO: handle errors op.MarkErrorAsHandled(); } else if (!op.IsCanceled) { this._source.Source = op.Entities; if (op.TotalEntityCount != -1) { this._view.SetTotalItemCount(op.TotalEntityCount); } } } private void OnSearch() { // This makes sure we refresh even if we're already on the first page using (this._view.DeferRefresh()) { // This will lead us to re-query for the total count this._view.SetTotalItemCount(-1); this._view.MoveToFirstPage(); } } #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { PropertyChangedEventHandler handler = this.PropertyChanged; if (handler != null) { handler(this, e); } } protected void RaisePropertyChanged(string propertyName) { this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } #endregion #region Sample data private class DesignTimeLoader : CollectionViewLoader { private static readonly IEnumerable source = new[] { new SampleEntity { Guid = Guid.NewGuid() }, new SampleEntity { Guid = Guid.NewGuid() }, new SampleEntity { Guid = Guid.NewGuid() }, }; public IEnumerable Source { get { return DesignTimeLoader.source; } } public override bool CanLoad { get { return true; } } public override void Load(object userState) { this.OnLoadCompleted(new AsyncCompletedEventArgs(null, false, userState)); } } #endregion #region RelayCommand // This is just a light version I wrote for this sample. In most cases you'll // want to use the RelayCommand type provided by an MVVM framework. private class RelayCommand : ICommand { private readonly Action _action; public RelayCommand(Action action) { this._action = action; } public event EventHandler CanExecuteChanged; public bool CanExecute(object parameter) { return true; } public void Execute(object parameter) { this._action(); } } #endregion } }
前台Xaml代码就不解释了,很简单,就是绑定到ViewModel,另外DataGrid和DataPager绑定对象是DomainCollectionView<T>。