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

带有过滤和排序功能的 WinRT CollectionView 类

starIconstarIconstarIconstarIconstarIcon

5.00/5 (20投票s)

2013 年 1 月 14 日

CPOL

19分钟阅读

viewsIcon

125917

downloadIcon

2531

本文介绍了一个支持筛选和排序的 ICollectionView 类。

引言

数据筛选和排序是 .NET 自十多年前推出以来就非常重要的功能。DataTable 类自 .NET 1.0 起就支持这两者。

当 WPF 出现时,它引入了 ICollectionView 接口,该接口除了排序和筛选外,还支持分组和数据游标(当前项的概念)。

令人惊讶的是,WinRT 版本系统库中指定的 ICollectionView 接口不支持排序、筛选或分组。在 WinRT 中,您可以在网格上显示项目列表,但没有标准的方法来排序或筛选这些数据。

本文介绍 ICollectionViewEx,它是 ICollectionView 接口的扩展版本,以及实现了该接口的 ListCollectionView 类的实现。有了这个类,您就可以像在 WPF、Silverlight 和 Windows Phone 应用程序中一样,为数据添加排序和筛选功能。

ListCollectionView 类还实现了 IEditableCollectionView 接口,该接口允许数据网格等高级控件实现高级编辑功能,例如取消编辑和添加新项。

本文包含一个示例,演示了如何使用 ListCollectionView 类实现类似 iTunes 等应用程序中找到的搜索框。搜索框将筛选应用于数据源,并选择包含用户在任何属性中键入的所有术语的项目。筛选后的数据可以用作任何控件的常规数据源,即使它们对 ICollectionViewEx 接口一无所知。

尽管该示例是 Windows 应用商店应用程序,但它使用了 MVVM 模型和 ICollectionViewEx 接口,这将使得创建 Silverlight、WPF 或 Windows Phone 的版本变得微不足道。

请注意,ListCollectionView 类不实现分组。这是一个更高级的功能,将留给读者作为练习。

ICollectionViewEx 接口

IColletionViewEx 接口继承自标准的 ICollectionView 接口,并添加了 WinRT 版本中缺失的成员。

/// <summary>
/// Extends the WinRT ICollectionView to provide sorting and filtering.
/// </summary>
public interface ICollectionViewEx : ICollectionView
{
  bool CanFilter { get; }
  Predicate<object> Filter { get; set; }

  bool CanSort { get; }
  IList<SortDescription> SortDescriptions { get; }

  bool CanGroup { get; }
  IList<object> GroupDescriptions { get; }

  IEnumerable SourceCollection { get; }

  IDisposable DeferRefresh();
  void Refresh();
}

除了我们稍后将实现的与筛选和排序相关的成员之外,该接口还包含与分组、公开视图的 SourceCollection 以及在视图被修改时刷新视图或延迟刷新相关的成员。

所有这些元素都存在于 WPF 版本的 ICollectionView 中,并且有许多库依赖于这些元素的可用性。

接口定义使用了 SortDescription 类和 ListSortDirection 枚举,这也必须被定义。

public class SortDescription
{
  public SortDescription(string propertyName, ListSortDirection direction)
  {
    PropertyName = propertyName;
    Direction = direction;
  }
  public string PropertyName { get; set; }
  public ListSortDirection Direction { get; set; }
}

public enum ListSortDirection
{
  Ascending = 0,
  Descending = 1,
}

IEditableCollectionView 接口

IEditableCollectionView 接口也从 WinRT 中被省略了。它公开了用于提供高级编辑(允许用户取消编辑)和向集合添加项的功能。

/// <summary>
/// Implements a WinRT version of the IEditableCollectionView interface.
/// </summary>
public interface IEditableCollectionView 
{
  bool CanAddNew { get; }
  bool CanRemove { get; }
  bool IsAddingNew { get; }
  object CurrentAddItem { get; }
  object AddNew();
  void CancelNew();
  void CommitNew();

  bool CanCancelEdit { get; }
  bool IsEditingItem { get; }
  object CurrentEditItem { get; }
  void EditItem(object item);
  void CancelEdit();
  void CommitEdit();
}

接口的第一部分与向集合添加项有关。它被 Grid 等控件使用,这些控件通常会公开一个用于新行的模板,用户可以通过填充模板来创建元素。

第二部分与编辑项有关。这一点很重要,因为您不希望在编辑项时对集合应用任何排序或筛选。这样做可能会导致项在集合中的位置发生变化,甚至在您完成编辑之前就被筛选出视图。此外,该接口定义了一个 CancelEdit 方法,该方法可以恢复对象的原始状态,撤销所有编辑。

ListCollectionView 类

ListCollectionView 类实现了 ICollectionViewExIEditableCollectionView 接口。它可以像普通的 WPF ListCollectionView 一样使用。例如:

// create a list
var list = new List<Rect>();
for (int i = 0; i < 10; i++)
    list.Add(new Rect(i, i, i, i));

// create a view that filters and sorts the list
var view = new ListCollectionView(list);
view.Filter = (item) => { return ((Rect)item).X > 5; };
view.SortDescriptions.Add(new SortDescription("X", ListSortDirection.Descending));

// show the result
foreach (var r in view)
    Console.WriteLine(r);

运行此代码会产生预期的输出。

 9,9,9,9
 8,8,8,8
 7,7,7,7
 6,6,6,6

ListCollectionView 类的工作原理如下:

  1. 它有一个源集合,其中包含一个元素列表。源集合由 SourceCollection 属性公开。(如果您希望能够通过添加和删除项来更改集合,则源集合应实现 INotifyCollectionChanged 接口,例如 ObservableCollection类)。
  2. 它有一个筛选器谓词,用于选择源集合中的哪些成员应包含在视图中。筛选器谓词是一个函数,它接受一个对象作为参数并返回 true(如果应包含该对象在视图中)或 false(否则)。默认情况下,筛选器设置为 null,这将导致所有元素都包含在视图中。筛选器谓词由 Filter 属性公开。
  3. 它有一个排序描述符集合,用于指定应使用哪些属性来排序包含在视图中的元素以及排序方向。排序描述符由 SortDescriptors 属性公开。
  4. 最后,视图是包含筛选和排序的元素列表。当上述三个元素中的任何一个发生更改时,它都会自动更新。实现 ListCollectionView 类所面临的主要挑战是高效地执行更新。

下图显示了这些元素如何交互。

ICollectionView diagram

ListCollectionView 类监听 SourceCollectionSortDescriptors 集合中的变化,以及 Filter 属性值的变化。

当检测到 SortDescriptorsFilter 发生变化时,View 集合会被完全重新生成,并且该类会向所有侦听器发出 Reset 通知。

当检测到 SourceCollection 发生变化时,该类会尝试执行最小化更新。

例如,如果向源添加了单个项,则会对其进行筛选。如果筛选器拒绝该项,则无需进一步操作。如果筛选器接受该项(或者没有筛选器),则该项会被插入到视图中的正确位置,并考虑当前的排序。在这种情况下,该类会发出 ItemAdded 通知。

同样,如果从源中删除了单个项并且该项存在于视图中,则该项将被简单地从视图中删除,并发出 ItemRemoved 通知。

最小化更新功能提高了应用程序性能,因为它最大限度地减少了类发出的完整刷新通知的数量。例如,设想一个显示数千个项的数据网格。可以处理添加项事件,方法是创建一个新行并将其插入到控件的正确位置。相比之下,完整刷新将要求控件丢弃其所有当前行,然后创建新的行。

现在我们知道了 ListCollectionView 的预期工作方式,让我们来看看实现。ListCollectionView 构造函数实现如下:

/// <summary>
/// Simple implementation of the <see cref="ICollectionViewEx"/> interface, 
/// which extends the standard WinRT definition of the <see cref="ICollectionView"/> 
/// interface to add sorting and filtering.
/// </summary>
public class ListCollectionView :
  ICollectionViewEx,
  IEditableCollectionView,
  IComparer<object>
  {
    public ListCollectionView(object source)
    {
      // create view (list exposed to consumers)
      _view = new List<object>();

      // create sort descriptor collection
      _sort = new ObservableCollection<SortDescription>();
      _sort.CollectionChanged += _sort_CollectionChanged;

      // hook up to data source
      Source = source;
    }
    public ListCollectionView() : this(null) { }

构造函数创建一个 _view 列表,该列表将包含筛选和排序后的输出列表。它还创建一个 _sort 集合,其中包含要应用于视图的排序描述符列表。_sort 集合是可观察的,因此每当它发生变化时,视图都可以刷新以显示新的排序顺序。

最后,构造函数将 Source 属性设置为源集合。以下是 Source 属性的实现方式:

/// <summary>
/// Gets or sets the collection from which to create the view.
/// </summary>
public object Source
{
 get { return _source; }
 set
 {
   if (_source != value)
   {
     // save new source
     _source = value;

     // listen to changes in the source
     if (_sourceNcc != null)
         _sourceNcc.CollectionChanged -= _sourceCollectionChanged;
     _sourceNcc = _source as INotifyCollectionChanged;
     if (_sourceNcc != null)
         _sourceNcc.CollectionChanged += _sourceCollectionChanged;

     // refresh the view
     HandleSourceChanged();
   }
 }
}

setter 存储对新源的引用,如果可用,则为其 CollectionChanged 事件连接一个处理程序,并调用 HandleSourceChanged 方法来填充视图。HandleSourceChanged 方法是事情开始变得有趣的地方。

// update view after changes other than add/remove an item 
void HandleSourceChanged()
{
  // keep selection if possible
  var currentItem = CurrentItem;

  // re-create view
  _view.Clear();
  var ie = Source as IEnumerable;
  if (ie != null)
  {
    foreach (var item in ie)
    {
      if (_filter == null || _filter(item))
      {
        if (_sort.Count > 0)
        {
          var index = _view.BinarySearch(item, this);
          if (index < 0) index = ~index;
            _view.Insert(index, item);
        }
        else
        {
          _view.Add(item);
        }
      }
    }
  }

  // notify listeners
  OnVectorChanged(VectorChangedEventArgs.Reset);

  // restore selection if possible
  CurrentItem = currentItem;
}

HandleSourceChanged 方法对视图执行完全刷新。它首先删除视图中的所有现有项。然后,它枚举源中的项,应用筛选器,并将它们添加到视图中。

如果 _sort 列表包含任何成员,则对视图进行排序,并通过调用 List 类提供的 BinarySearch 方法来确定新项应插入的位置。

最后,该方法调用 OnVectorChanged 成员来引发 VectorChanged 事件,该事件负责通知绑定到视图的任何客户端。

如果我们不关心效率,我们可以在这里停止。在源集合或筛选器/排序参数发生任何更改后调用 HandleSourceChanged 方法即可工作。唯一的问题是它的速度会很慢。例如,当单个项被添加到源或从中移除时,绑定到视图的任何控件都必须执行完全刷新。

ListCollectionView 类具有非常高效地处理项添加和移除的方法。这些方法实现如下:

// remove item from view
void HandleItemRemoved(int index, object item)
{
  // no update needed if the item was filtered out of the view
  if (_filter != null && !_filter(item))
    return;

  // compute index into view
  if (index < 0 || index >= _view.Count || !object.Equals(_view[index], item))
    index = _view.IndexOf(item);
  if (index < 0)
    return;

  // remove item from view
  _view.RemoveAt(index);

  // if item was below our cursor, update cursor 
  if (index <= _index)
    _index--;

  // notify listeners
  var e = new VectorChangedEventArgs(CollectionChange.ItemRemoved, index, item);
  OnVectorChanged(e);
}

HandleItemRemoved 方法首先检查从源中移除的项是否未被筛选出视图。如果是这种情况,那么视图没有改变,不需要更新任何内容。

接下来,该方法确定已移除项在当前视图中的索引。如果视图被筛选或排序,则传递给方法的索引将无效,并且通过调用 IndexOf 方法来确定实际索引。一旦知道了项的索引,该项就会从视图中移除。

如果移除的项位于视图当前项之上(由 _index 变量确定),则调整视图索引,以便当前项保持为当前项。在这种情况下,视图的 CurrentPosition 属性会发生变化,但 CurrentItem 属性将保持不变。

最后,该方法调用 OnVectorChanged 事件来通知侦听器视图已发生更改。

HandleItemAdded 方法负责在项添加到源集合时更新视图。

// add item to view
void HandleItemAdded(int index, object item)
{
  // if the new item is filtered out of view, no work
  if (_filter != null && !_filter(item))
    return;

  // compute insert index
  if (_sort.Count > 0)
  {
    // sorted: insert at sort position
    _sortProps.Clear();
    index = _view.BinarySearch(item, this);
    if (index < 0) index = ~index;
  }
  else if (_filter != null)
  {
    var visibleBelowIndex = 0;
    for (int i = index; i < _sourceList.Count; i++)
    {
      if (!_filter(_sourceList[i]))
        visibleBelowIndex++;
    }
    index = _view.Count - visibleBelowIndex;
  }

  // add item to view
  _view.Insert(index, item);

  // keep selection on the same item
  if (index <= _index)
    _index++;

  // notify listeners
  var e = new VectorChangedEventArgs(CollectionChange.ItemInserted, index, item);
  OnVectorChanged(e);
}

与之前一样,该方法首先检查添加到原始集合的项是否应包含在视图中。如果不是,则无需执行任何操作。

接下来,该方法确定新项在视图中应有的索引。如果视图已排序,则通过调用 BinarySearch 方法来确定索引,如前所述。无论视图是否经过筛选,这都会起作用。

如果视图未排序但已筛选,则通过计算源集合中有多少项位于新项下方且未被筛选出视图来确定项在视图中的索引。然后,通过将此数量从视图中的项数中减去来获得项在视图中的索引。这可确保项在视图中的显示顺序与它们在源列表中的显示顺序相同。

一旦知道了项的索引,就会将该项添加到视图中。

与之前一样,如果新项插入到视图当前项的上方,则会更新视图的索引。

最后,该方法调用 OnVectorChanged 事件来通知侦听器视图已发生更改。

现在我们已经有了这三个更新方法,下一步是在正确的位置调用它们:首先,当排序描述符集合或筛选器谓词更改时,我们调用 HandleSourceChanged。在这两种情况下,视图都必须完全刷新。

/// <summary>
/// Gets or sets a callback used to determine if an item is suitable for
/// inclusion in the view.
/// <summary>
public Predicate<object> Filter
{
  get { return _filter; }
  set
  {
    if (_filter != value)
    {
       _filter = value;
       HandleSourceChanged();
    }
  }
}
// sort changed, refresh view
void _sort_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
  HandleSourceChanged();
}

接下来,当源集合发生变化时,我们调用相应的方法。

// the source has changed, update view
void _sourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
  switch (e.Action)
  {
    case NotifyCollectionChangedAction.Add:
      if (e.NewItems.Count == 1)
        HandleItemAdded(e.NewStartingIndex, e.NewItems[0]);
      else
        HandleSourceChanged();
      break;

    case NotifyCollectionChangedAction.Remove:
      if (e.OldItems.Count == 1)
        HandleItemRemoved(e.OldStartingIndex, e.OldItems[0]);
      else
        HandleSourceChanged();
      break;

    case NotifyCollectionChangedAction.Move:
    case NotifyCollectionChangedAction.Replace:
    case NotifyCollectionChangedAction.Reset:
      HandleSourceChanged();
      break;

    default:
      throw new Exception(
        "Unrecognized collection change notification" +
        e.Action.ToString());
  }
}

最后,我们调用 HandleSourceChanged 方法来响应公共的 Refresh 方法。

/// <summary>
/// Update the view from the current source, using the current filter 
/// and sort settings.
/// </summary>
public void Refresh()
{
  HandleSourceChanged();
}

这标志着筛选和排序逻辑实现的结束。

延迟通知

延迟通知允许调用者在进行大量视图更改时暂停更改通知。例如,在批量添加项或应用多个筛选器定义时,暂停通知通常是个好主意。

ICollectionViewEx 接口中的延迟通知机制由一个成员公开,即 DeferRefresh 方法。该方法实现如下:

/// <summary>
/// Enters a defer cycle that you can use to merge changes to the
/// view and delay automatic refresh.
/// </summary>
public IDisposable DeferRefresh()
{
  return new DeferNotifications(this);
}
/// <summary>
/// Class that handles deferring notifications while the view is modified.
/// </summary>
class DeferNotifications : IDisposable
{
  ListCollectionView _view;
  object _currentItem;
  internal DeferNotifications(ListCollectionView view)
  {
    _view = view;
    _currentItem = _view.CurrentItem;
    _view._updating++;
  }
  public void Dispose()
  {
    _view.MoveCurrentTo(_currentItem);
    _view._updating--;
    _view.Refresh();
  }
}

DeferRefresh 方法返回一个实现了 IDisposable 接口的内部 DeferNotifications 对象。用法模式如下:

using (view.DeferRefresh())
{
  // make extensive modifications to the view
}

调用 DeferRefresh 会创建一个 DeferNotifications 对象,该对象会递增 ListCollectionView 中的 _updating 计数器。只要 _updating 计数器大于零,视图就不会发出任何通知。

块的末尾,DeferNotifications 对象超出范围,自动调用其 Dispose 方法。Dispose 方法会递减 _updating 计数器并调用 Refresh 方法来恢复更新。

这种模式优于备用的 BeginUpdate/EndUpdate 方法,因为它使暂停通知的代码块的作用域非常容易界定。它还可以确保即使代码块内出现异常,通知也能正确恢复(您无需编写显式的“finally”子句)。

其他 ICollectionView 方法

以上部分讨论了 ICollectionViewEx 中存在的排序和筛选方法的实现,而这些方法在 ICollectionView 的 WinRT 版本中不存在。

由于 ICollectionViewEx 接口继承自 ICollectionView,因此我们的 ListCollectionView 类也必须实现这些方法。

幸运的是,这些方法相对简单。它们分为两大类:

  1. 列表操作ListCollectionView 类将列表操作委托给其 _sourceList 字段,该字段只是源集合强制转换为 IList 对象,提供了所有必要的方法(如 AddRemoveContainsIndexOf 等)。如果源集合不是 IList,则 IsReadOnly 属性将返回 true,并且这些方法将不可用。
  2. 游标操作ListCollectionView 类跟踪当前选中的项,并通过 CurrentItemCurrentPosition 等成员、几个 MoveCurrentTo 方法以及 CurrentChanging/CurrentChanged 事件公开此信息。所有这些属性、方法和事件都由前面提到的 _index 属性控制。

由于这些方法非常简单,我们在此不一一列出。如果您对实现细节感兴趣,请参阅源代码。

IEditableCollectionView 实现

IEditableCollectionView 的实现相对简单。该接口的第一部分与编辑项有关。代码如下:

// object being edited:
object _editItem;

public bool CanCancelEdit { get { return true; } }
public object CurrentEditItem { get { return _editItem; } }
public bool IsEditingItem { get { return _editItem != null; } }
public void EditItem(object item)
{
  var ieo = item as IEditableObject;
  if (ieo != null && ieo != _editItem)
    ieo.BeginEdit();
  _editItem = item;
}
public void CancelEdit()
{
  var ieo = _editItem as IEditableObject;
  if (ieo != null)
    ieo.CancelEdit();
  _editItem = null;
}
public void CommitEdit()
{
  if (_editItem != null)
  {
    var item = _editItem;
    var ieo = item as IEditableObject;
    if (ieo != null)
      ieo.EndEdit();
   _editItem = null;
    HandleItemChanged(item);
  }
}

实现包括跟踪正在编辑的对象并在适当的时候调用其 IEditableObject 方法。例如,这允许用户在数据网格中编辑对象时按 Esc 键,以取消所有编辑并恢复对象的原始状态。

CommitEdit 方法调用 HandleItemChanged 方法,以确保新项在视图中得到正确的筛选和排序。

IEditableCollectionView 接口的第二部分与向视图添加项有关,实现如下:

// object being added:
object _addItem;

public bool CanAddNew { get { return !IsReadOnly && _itemType != null; } }
public object AddNew()
{
  _addItem = null;
  if (_itemType != null)
  {
    _addItem = Activator.CreateInstance(_itemType);
    if (_addItem != null)
      this.Add(_addItem);
  }
  return _addItem;
}
public void CancelNew()
{
  if (_addItem != null)
  {
    this.Remove(_addItem);
    _addItem = null;
  }
}
public void CommitNew()
{
  if (_addItem != null)
  {
    var item = _addItem;
    _addItem = null;
    HandleItemChanged(item);
}
}
public bool CanRemove { get { return !IsReadOnly; } }
public object CurrentAddItem { get { return _addItem; } }
public bool IsAddingNew { get { return _addItem != null; } }

AddNew 方法使用 Activator.CreateInstance 方法创建适当类型的新元素。新项被追加到视图中,直到调用 CommitNew 方法才进行排序或筛选。

此逻辑允许数据网格等控件提供“新行”模板。当用户开始在模板中键入时,一个项会自动添加到视图中。当用户将光标移动到网格上的新行时,它会调用 CommitNew 方法并刷新视图。如果用户在提交新行之前按 Esc 键,数据网格会调用 CancelNew 方法,新项将从视图中移除。

MyTunes 示例应用程序

为了演示如何使用 ListCollectionView 类,我们创建了一个名为 **MyTunes** 的简单 MVVM 应用程序。该应用程序从资源文件中加载歌曲列表,并在 GridView 控件中显示歌曲。

用户可以通过在搜索框中键入术语来搜索歌曲。例如,键入“hendrix love”将只显示在标题、专辑或艺术家名称中包含“hendrix”和“love”的歌曲。用户还可以通过单击列表上方的任一按钮来按标题、专辑或艺术家对歌曲进行排序。

下图显示了应用程序的外观。

MyTunes application screenshot

MyTunes ViewModel

ViewModel 类公开一个歌曲集合以及筛选和排序集合的方法。这是声明和构造函数:

public class ViewModel : INotifyPropertyChanged
{
  ListCollectionView _songs;
  string _filterTerms;
  Storyboard _sbUpdateFilter;
  ICommand _cmdSort;

  // ** ctor
  public ViewModel()
  {
    // expose songs as an ICollectionViewEx
    _songs = new ListCollectionView();
    _songs.Source = Song.GetAllSongs();
    _songs.Filter = FilterSong;

    // sort by Artist by default
    var sd = new SortDescription("Artist", ListSortDirection.Ascending);
    _songs.SortDescriptions.Add(sd);

    // use a storyboard to update filter after a delay
    _sbUpdateFilter = new Storyboard();
    _sbUpdateFilter.Duration = new Duration(TimeSpan.FromSeconds(1));
    _sbUpdateFilter.Completed += (s,e) => 
    {
      // refresh collection view to apply updated filter
      _songs.Refresh();
    };

    // command to sort the view
    _cmdSort = new SortCommand(this);
  }

构造函数开始声明一个 ListCollectionView 来保存歌曲,将其 Source 属性设置为从本地资源加载的原始歌曲列表,并将 Filter 属性设置为 FilterSong 方法,该方法负责选择将包含在视图中的歌曲。它还初始化 SortDescriptions 属性以默认按艺术家对歌曲进行排序。

接下来,构造函数设置一个 StoryBoard,用于在用户停止更改搜索词一秒后刷新视图。这比在每次击键后刷新列表更有用。

最后,构造函数创建一个 ICommand 对象,该对象负责根据不同属性对视图进行排序。

ViewModel 类的对象模型实现如下:

// ** object model
public ICollectionView Songs
{
  get { return _songs; }
}
public ICommand SortBy
{
  get { return _cmdSort; }
}
public string FilterTerms
{
  get { return _filterTerms; }
  set
  {
    if (value != FilterTerms)
    {
      // change the property
      _filterTerms = value;
      OnPropertyChanged("FilterTerms");

      // start timer to update the filter after one second
      _sbUpdateFilter.Stop();
      _sbUpdateFilter.Seek(TimeSpan.Zero);
      _sbUpdateFilter.Begin();
    }
  }
}

ViewModel 只有三个属性。

Songs 属性公开了作为标准 ICollectionView 的筛选和排序集合。这是将绑定到显示歌曲的控件的 ItemsSource 属性的属性。在我们的示例中,这将是 GridView 控件。

SortBy 属性公开了一个 ICommand 对象,该对象将绑定到用于排序集合的按钮上的 Command 属性。

最后,FilterTerms 属性包含一个字符串,其中包含用于搜索列表的术语。当属性值更改时,代码会启动一个 Storyboard,该 Storyboard 将在延迟一秒后刷新视图。这样做是为了让用户可以在搜索框中键入,而不会在每次击键后刷新视图。

实现的其他部分如下:

// ** implementation
bool FilterSong(object song)
{
  // no filter term, pass always
  if (string.IsNullOrEmpty(FilterTerms))
    return true;

  // test each term in the filter terms
  foreach (var term in this.FilterTerms.Split(' '))
  {
    if (!FilterSongTerm((Song)song, term))
      return false;
  }

  // passed all
  return true;
}
static bool FilterSongTerm(Song song, string term)
{
  return 
    string.IsNullOrEmpty(term) ||
    song.Name.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 ||
    song.Album.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 ||
    song.Artist.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propName)
{
  if (PropertyChanged != null)
    PropertyChanged(this, new PropertyChangedEventArgs(propName));
}}

FilterSong 方法(在构造函数中分配给 ListCollectionViewFilter 属性)负责确定应将哪些歌曲包含在视图中。它通过将筛选术语拆分为字符串数组来做到这一点,并仅对在其 NameAlbumArtist 属性中包含所有术语的歌曲返回 true。

ViewModelSortBy 属性公开的 SortCommand 类实现如下:

class SortCommand : ICommand
{
  ViewModel _vm;
  public SortCommand(ViewModel vm)
  {
    _vm = vm;

    // update CanExecute value when the collection is refreshed
    var cv = _vm.Songs as ListCollectionView;
    if (cv != null)
    {
      cv.VectorChanged += (s, e) =>
      {
        if (CanExecuteChanged != null)
          CanExecuteChanged(this, EventArgs.Empty);
      };
    }
  }
  public event EventHandler CanExecuteChanged;
  public bool CanExecute(object parameter)
  {
    var prop = parameter as string;
    var cv = _vm.Songs as ListCollectionView;

    // check that we have a property to sort on
    if (cv == null || string.IsNullOrEmpty(prop))
      return false;

    // check that we are not already sorted by this property
    if (cv.SortDescriptions.Count > 0 &&
      cv.SortDescriptions[0].PropertyName == prop)
      return false;

    // all seems OK
    return true;
  }
  public void Execute(object parameter)
  {
    var prop = parameter as string;
    var cv = _vm.Songs as ListCollectionView;
    if (cv != null && !string.IsNullOrEmpty(prop))
    {
      using (cv.DeferRefresh())
      {
        cv.SortDescriptions.Clear();
        var sd = new SortDescription(
          prop, 
          ListSortDirection.Ascending);
        cv.SortDescriptions.Add(sd);
      }
    }
  }
}

该类实现了一个 CanExecute 方法,该方法在集合已按给定参数排序时返回 false,否则返回 true。这会导致绑定到命令的按钮在视图已按该参数排序时自动禁用。例如,单击“按艺术家排序”按钮将按艺术家对集合进行排序,并且在列表按其他属性排序之前,该按钮将保持禁用状态。

Execute 方法的实现包括更新 ListCollectionViewSortDescriptions 属性。请注意,这是在 DeferRefresh 块内完成的,因此视图只会刷新一次。

MyTunes 视图

视图以纯 XAML 实现。有趣的部分列于下文:

<Page
  x:Class="MyTunes.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:local="using:MyTunes"
  mc:Ignorable="d">

  <Page.Resources>
    <local:ViewModel x:Key="_vm" />
    <local:DurationConverter x:Key="_cvtDuration" />
  </Page.Resources>

  <Grid 
    Background="{StaticResource ApplicationPageBackgroundThemeBrush}" 
    DataContext="{StaticResource _vm}">

    <Grid.RowDefinitions…>
      <RowDefinition Height="Auto" />
      <RowDefinition />
    </Grid.RowDefinitions>

此代码创建 ViewModel 类的实例,并将其分配给将作为布局根的元素的 DataContext 属性。

页面内容如下:

<Grid Margin="20">
  <Grid.ColumnDefinitions>
    <ColumnDefinition />
    <ColumnDefinition Width="auto"/>
  </Grid.ColumnDefinitions>

  <TextBlock Text="MyTunes" FontSize="48" />

  <StackPanel Orientation="Horizontal"
    Grid.Column="1" Margin="12" VerticalAlignment="Center">

    <TextBlock Text="Sort" />
    <Button Content="By Song" 
       Command="{Binding SortBy}" CommandParameter="Name"/>
    <Button Content="By Album" 
       Command="{Binding SortBy}" CommandParameter="Album"/>
    <Button Content="By Artist" 
       Command="{Binding SortBy}" CommandParameter="Artist"/>

    <TextBlock Text="Search" />
    <local:ExtendedTextBox 
      Width="300" Margin="8 0" VerticalAlignment="Center" 
      Text="{Binding FilterTerms, Mode=TwoWay }" />
  </StackPanel>
</Grid>

第一个元素是一个包含应用程序标题和命令栏的网格。

命令栏包含三个绑定到 ViewModelSortBy 属性的按钮,用于按歌曲的 NameAlbumArtist 对视图进行排序。由于前面介绍的 SortCommand 类,当视图已按其表示的属性排序时,这些按钮会自动禁用。

在排序按钮之后,命令栏包含一个绑定到 ViewModelFilterTerms 属性的文本框。

请注意,我们没有使用标准的 TextBox 控件。原因是 WinRT TextBox 仅在失去焦点时才更新其绑定源。在我们的应用程序中,应在用户键入时更新筛选器。为了获得我们想要的即时更新行为,我们使用了 CodePlex 上提供的 ExtendedTextBox 控件。

https://mytoolkit.svn.codeplex.com/svn/WinRT/Controls/ExtendedTextBox.cs

视图的最后一部分是用于显示歌曲的 GridView 元素。

<GridView ItemsSource="{Binding Songs}" Grid.Row="1" >
  <GridView.ItemTemplate>
    <DataTemplate>
      <Border Margin="10" Padding="20" Background="#20c0c0c0" >
        <StackPanel Width="350">
          <TextBlock Text="{Binding Name}" FontSize="20" />
          <TextBlock Text="{Binding Album}" />
          <TextBlock>
            <Run Text="{Binding Artist}" />
              <Run Text=" (" />
              <Run Text="{Binding Duration,
                Converter={StaticResource _cvtDuration}}" />
              <Run Text=")" />
            </TextBlock>
          </StackPanel>
        </Border>
      </DataTemplate>
    </GridView.ItemTemplate>
  </GridView>
</Grid>

GridView 元素绑定到 ViewModelSongs 属性。ItemTemplate 包含绑定到 Song 类属性的 TextBlock 元素。

这就是完整的应用程序。该页面没有代码隐藏,这在 MVVM 风格的应用程序中很常见。事实上,这个应用程序将是一个完全标准的 Silverlight 或 WPF MVVM 应用程序。唯一使其有趣的是它是 WinRT(Windows 应用商店)应用程序,并且其 ViewModel 提供了筛选和排序功能,而这些功能并未得到 WinRT 的原生支持。这项工作由我们的 ListCollectionView 类处理。

FilterControl 示例应用程序

除了 MyTunes 示例外,您可能还想在此处查看另一个有趣的示例:

http://our.componentone.com/samples/winrtxaml-filter

此示例显示了如何在 WinRT 中实现触摸友好的 FilterControlFilterControl 绑定到一个集合视图。当用户修改筛选器时,控件会更新集合视图的 Filter 属性,任何绑定到集合视图的控件都会自动显示筛选后的结果。

这是示例中 FilterControl 的外观。

FilterControl for WinRT

要使用 FilterControl,用户从左侧列表(例如“Country”)中选择一个属性。这会导致筛选器显示当前视图中该属性值的直方图(例如,每个国家的销售额)。然后,用户通过滑动直方图来选择一个特定值(例如,德国的销售额)。

整个过程使得无需键入即可轻松通过滑动操作筛选数据,这使得这种类型的控件非常适合在平板电脑和手机应用程序中使用。

FilterControl 允许您将 ValueConverter 对象附加到每个属性,因此您可以创建显示大陆而不是特定国家/地区的直方图,或描述值范围的标签(例如,高、中、低销售额)。

请注意,示例中的 FilterControl 没有使用我们的 ICollectionViewEx 接口,而是使用了商业包(ComponentOne Studio for WinRT)中定义的类似接口。如果您想使用本文介绍的 ListCollectionView 类来运行该示例,您需要对 FilterControl 类进行一些小的编辑。

由于这超出了本文的范围,我们不会深入研究 FilterControl 示例的细节。但如果您觉得它有用,请随时从上面的链接下载源代码并使用该控件。

结论

WinRT 是一个令人兴奋的新开发平台。对我来说,它最有价值的承诺之一是能够重新利用为 WPF 和 Silverlight 应用程序开发的现有代码。不幸的是,在这方面存在一些困难。

一个问题是许多名称已更改(命名空间、类名、属性、事件、方法等)。这些问题通常可以通过向代码添加 #if 块来解决。这样做可以,但您的代码会变得有些混乱,更难维护和调试。

第二个也是更严重的问题是,WinRT 中缺少一些重要的功能。一个很好的例子是 TextBox 控件在文本更改时而不是在控件失去焦点时更新其绑定值的能力。在我们的示例应用程序中,我们使用 CodePlex 上的 ExtendedTextBox 解决了这个问题。

另一个例子当然是 ICollectionView 接口的重新定义,这也是本文的起因。在我看来,WinRT 设计者应该保留原始定义。他们可以通过让 CanFilterCanSortCanGroup 属性始终返回 false 来跳过在其自己的类中实现筛选、排序和分组方法。我们仍然需要实现该功能,但至少我们不必定义新版本的接口。这种方法将使将 WPF 和 Silverlight 控件移植到 WinRT 更加容易。

也许未来版本的 WinRT 会恢复 ICollectionView 接口的筛选、排序和分组功能。在此之前,我们将不得不依赖自定义实现,例如 ListCollectionView

© . All rights reserved.