WPF数据表格绑定的数据发生变化后重新排序

34
我正在寻找一种方法来在底层数据 更改 时重新排序我的 DataGrid
(这个设置是相当标准的:DataGrid 的 ItemSource 属性绑定到一个 ObservableCollection 上;列使用 DataGridTextColumns;DataGrid 内部的数据对 ObservableCollection 中的更改做出了正确反应;通过鼠标点击可以正常进行排序)
有什么想法吗?

5
你有没有探索过CollectionViewSource - WPF-it
@WPF-it 现在我做到了 ;) 你给了我正确的提示,我能够用 CollectionViewSource 解决我的问题。请查看我的答案以了解我所做的。谢谢! - marc wellman
5个回答

32

我花了整个下午,但最终我找到了一个出人意料的简单短小精悍高效的解决方案:

要控制所涉及的UI控件(在这里是DataGrid)的行为,可以简单地使用CollectionViewSource。它作为ViewModel中UI控件的代表,而不完全打破MVVM模式。

在ViewModel中声明一个CollectionViewSource和一个普通的ObservableCollection<T>,并将CollectionViewSource包装在ObservableCollection周围:

// Gets or sets the CollectionViewSource
public CollectionViewSource ViewSource { get; set; }

// Gets or sets the ObservableCollection
public ObservableCollection<T> Collection { get; set; }

// Instantiates the objets.
public ViewModel () {

    this.Collection = new ObservableCollection<T>();
    this.ViewSource = new CollectionViewSource();
    ViewSource.Source = this.Collection;
}

然后在应用程序的视图部分,你只需要将CollectionControlItemsSource绑定到CollectionViewSource的视图属性,而不是直接绑定到ObservableCollection,其他什么都不用做:

<DataGrid ItemsSource="{Binding ViewSource.View}" />

从现在开始,您可以在ViewModel中使用CollectionViewSource对象来直接操作View中的UI控件。

例如,排序-就像我的主要问题一样-将如下所示:

// Specify a sorting criteria for a particular column
ViewSource.SortDescriptions.Add(new SortDescription ("columnName", ListSortDirection.Ascending));

// Let the UI control refresh in order for changes to take place.
ViewSource.View.Refresh();

你看,非常简单和直观。希望这可以像帮助我一样帮助其他人。


这很棒。唯一的问题是,一旦您将ViewSource.Source设置为ObservableCollection,您就无法在BackgroundWorker中更改ObservableCollection。或者至少我不能。 - Sandra
1
如果您直接将控件绑定到ObservableCollection,也会发生这种情况。在这两种情况下,您可以通过在应用程序调度程序上调用操作来实际更改集合。 - RickNo
嗨,Marc Wellman,我正在尝试使用WPF datagrid来实现您的排序方法,但datagrid甚至没有显示我绑定到它的数据,更不用说排序了。之前我使用的是基本的非排序datagrid,使用gridName.ItemsSource = myObservableCollection;以及gridName.Items.Refresh();,使用这种方法可以正确加载数据,但当我切换到您的方法时,datagrid根本没有显示任何数据。我在考虑是否应该开一个新帖子,这样我就可以展示我的代码并引用这个帖子,但不确定在stackoverflow世界中是否合适。有什么建议吗? - David Pugh
它正在运行,但刷新需要很长时间。我的DataGrid中只有43条记录。 - Colonel Software
我和@C有同样的问题。 - RDV
Refresh()会使您失去在Refresh()之前选择的行信息。 - Peter Huber

21

这更多是为了澄清而不是回答,但 WPF 始终 绑定到 ICollectionView 而不是源集合。 CollectionViewSource 是用于创建/检索集合视图的机制。

这是一个关于该主题的好资源,它应该帮助你更好地使用 WPF 中的集合视图:http://bea.stollnitz.com/blog/?p=387

在 XAML 中使用 CollectionViewSource 实际上可以简化你的代码:

<Window.Resources>
    <CollectionViewSource Source="{Binding MySourceCollection}" x:Key="cvs">
      <CollectionViewSource.SortDescriptions>
        <scm:SortDescription PropertyName="ColumnName" />
      </CollectionViewSource.SortDescriptions>
    </CollectionViewSource>
</Window.Resources>

...

<DataGrid ItemsSource="{Binding Source={StaticResource cvs}}">
</DataGrid>
有人认为,遵循MVVM模式时,视图模型应该始终公开集合视图,但我认为这取决于使用情况。如果视图模型永远不会直接与集合视图交互,那么在XAML中配置它会更容易。

这是一个很好的建议 - CollectionViewSource 应该成为 View 代码后台中的属性(可以绑定到 VM 上的 ObservableCollection),或者像你所做的那样将其声明为本地静态资源。 - slugster
这是最佳解决方案,只有一个错误在你的XAML中 - 在SortDescription元素中应该是“PropertyName”而不是“Property”。 - sdds
6
谢谢!这很有帮助。我只是在弄清楚scm命名空间时遇到了一些困难。以下是供试图找到它的人使用的命名空间:xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"。 - Mario
4
这个链接已经无效了,但我在[互联网档案馆]上找到了这篇文章的文本。(https://web.archive.org/web/20150103043436/http://www.zagstudio.com/blog/387#.VwVFQfkrKUk) - Zach Mierzejewski

6

sellmeadog的回答要么过于复杂,要么已经过时了。其实很简单,你只需要:

<UserControl.Resources>
    <CollectionViewSource 
        Source="{Binding MyCollection}" 
        IsLiveSortingRequested="True" 
        x:Key="MyKey" />
</UserControl.Resources>

<DataGrid ItemsSource="{Binding Source={StaticResource MyKey} }" >...

1
只是一点提示,IsLiveSortingRequested 需要 .NET 4.5。 - Speerian
它是如何知道按哪一列排序的?它不需要在CollectionViewSource SortDescription中指定吗? - njplumridge
实际上,您没有提到它如何识别要排序的列。 - Cosmin
1
根据问题所述,用户通过单击列标题选择排序方式。在这种情况下,无需在CollectionViewSource中定义排序方式。 - Peter Huber

0

我看不到任何明显简单的方法,所以我会尝试使用附加行为。这有点不太正常,但可以给你想要的结果:

public static class DataGridAttachedProperty
{
     public static DataGrid _storedDataGrid;
     public static Boolean GetResortOnCollectionChanged(DataGrid dataGrid)
     {
         return (Boolean)dataGrid.GetValue(ResortOnCollectionChangedProperty);
     }

     public static void SetResortOnCollectionChanged(DataGrid dataGrid, Boolean value)
     {
         dataGrid.SetValue(ResortOnCollectionChangedProperty, value);
     }

    /// <summary>
    /// Exposes attached behavior that will trigger resort
    /// </summary>
    public static readonly DependencyProperty ResortOnCollectionChangedProperty = 
         DependencyProperty.RegisterAttached(
        "ResortOnCollectionChangedProperty", typeof (Boolean),
         typeof(DataGridAttachedProperty),
         new UIPropertyMetadata(false, OnResortOnCollectionChangedChange));

    private static void OnResortOnCollectionChangedChange
        (DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
      _storedDataGrid = dependencyObject as DataGrid;
      if (_storedDataGrid == null)
        return;

      if (e.NewValue is Boolean == false)
        return;

      var observableCollection = _storedDataGrid.ItemsSource as ObservableCollection;
      if(observableCollection == null)
        return;
      if ((Boolean)e.NewValue)
        observableCollection.CollectionChanged += OnCollectionChanged;
      else
        observableCollection.CollectionChanged -= OnCollectionChanged;
    }

    private static void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
      if (e.OldItems == e.NewItems)
        return;

      _storedDataGrid.Items.Refresh()
    }
}

然后,您可以通过以下方式附加它:

<DataGrid.Style>
  <Style TargetType="DataGrid">
    <Setter 
      Property="AttachedProperties:DataGridAttachedProperty.ResortOnCollectionChangedProperty" 
                                    Value="true" 
      />
   </Style>
 </DataGrid.Style>

非常感谢您详细的回答,但不幸的是我无法使其工作。我设法将您的属性附加到我的DataGrid,并在底层集合的每个CollectionChange事件上调用_storedDataGrid.ItemsCollection.Refresh()。但由于某种原因,DataGrid无法排序。我不确定,但可能是因为我正在使用多个线程,并且在将Refresh调用传播到UIThread时遇到了一些问题。无论如何,我找到了另一个很好的解决方案-请参阅我的答案。再次感谢您! - marc wellman

-1

对于其他遇到这个问题的人,以下内容可能会帮助你开始解决... 如果你有一个 INotifyPropertyChanged 集合,你可以使用它来代替 ObservableCollection - 当集合中的单个项发生更改时,它将自动刷新: 注意:由于这会将项标记为已删除然后重新添加(即使它们实际上并没有被删除和添加),所以选择可能会失去同步。对于我的小型个人项目来说已经足够好了,但还不准备发布给客户...

public class ObservableCollection2<T> : ObservableCollection<T>
{
    public ObservableCollection2()
    {
        this.CollectionChanged += ObservableCollection2_CollectionChanged;
    }

    void ObservableCollection2_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems != null)
            foreach (object o in e.OldItems)
                remove(o);
        if (e.NewItems != null)
            foreach (object o in e.NewItems)
                add(o);
    }
    void add(object o)
    {
        INotifyPropertyChanged ipc = o as INotifyPropertyChanged;
        if(ipc!=null)
            ipc.PropertyChanged += Ipc_PropertyChanged;
    }
    void remove(object o)
    {
        INotifyPropertyChanged ipc = o as INotifyPropertyChanged;
        if (ipc != null)
            ipc.PropertyChanged -= Ipc_PropertyChanged;
    }
    void Ipc_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        NotifyCollectionChangedEventArgs f;

        f = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, sender);
        base.OnCollectionChanged(f);
        f = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, sender);
        base.OnCollectionChanged(f);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接