带有 ICollectionView SortDescription 的 Datagrid 丢失了 - Bug?

bli*_*eis 4 c# wpf mvvm icollectionview

这就是我想要的:如果我将 ICollectionview 绑定到 DataGrid,我不想丢失 Viewmodel 中的 SortDescription。

\n\n

我创建了一个小示例项目来看看我的意思。在我的项目中,我只是使用用户控件在数据网格中显示我的数据。如果我这样做,当 UserControl 卸载时, SortDescription 就会消失,因为 ItemsSource 设置为 null。如果我使用 TemplateSelector 显示我的 UserControl,则 SortDescription不会消失,并且 ItemsSource 在卸载时不会设置为 null。问题是,为什么会有这些不同的行为?这两种行为中的一种是错误吗?

\n\n

顺便提一句。我使用 .Net 4.5.1 但安装了 4.6.1 和 system.Windows.Interactivity 4.0.0.0

\n\n

主窗口.xaml

\n\n
<Window x:Class="DataGridICollectionView.MainWindow"\n    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"\n    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"\n    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"\n    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"\n    xmlns:local="clr-namespace:DataGridICollectionView"\n    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"\n    mc:Ignorable="d"\n    Title="MainWindow" Height="350" Width="525">\n<Window.Resources>\n    <DataTemplate DataType="{x:Type local:ViewmodelListe}">\n        <local:MyViewUc/>\n    </DataTemplate>\n</Window.Resources>\n<Grid>\n    <Grid.RowDefinitions>\n        <RowDefinition Height="Auto"/>\n        <RowDefinition Height="Auto"/>\n    </Grid.RowDefinitions>\n\n    <ToolBar Grid.Row="0">\n        <Button Content="SetWorkspace MyView" Click="Button_Click"/>\n        <Button Content="SetWorkspace Other" Click="Button_Click_1"/>\n    </ToolBar>\n\n    <ContentPresenter Grid.Row="1" Content="{Binding Workspace}"/>\n</Grid>\n</Window>\n
Run Code Online (Sandbox Code Playgroud)\n\n

MainWindow.xaml.cs

\n\n
namespace DataGridICollectionView\n{\n/// <summary>\n/// Interaktionslogik f\xc3\xbcr MainWindow.xaml\n/// </summary>\npublic partial class MainWindow : Window, INotifyPropertyChanged\n{\n    private object _workspace;\n\n    public MainWindow()\n    {\n        InitializeComponent();\n        MyViewVm = new ViewmodelListe();\n\n        DataContext = this;\n    }\n\n    public ViewmodelListe MyViewVm { get; set; }\n\n    public object Workspace\n    {\n        get { return _workspace; }\n        set\n        {\n            _workspace = value;\n            OnPropertyChanged();\n        }\n    }\n\n    private void Button_Click(object sender, RoutedEventArgs e)\n    {\n        Workspace = MyViewVm;\n    }\n\n    private void Button_Click_1(object sender, RoutedEventArgs e)\n    {\n        Workspace = "Other";\n    }\n\n    public event PropertyChangedEventHandler PropertyChanged;\n\n    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)\n    {\n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n    }\n}\n\npublic class ViewmodelListe : INotifyPropertyChanged\n{\n    public ViewmodelListe()\n    {\n        Persons = new ObservableCollection<Person>();\n        MyView = CollectionViewSource.GetDefaultView(Persons);\n\n        Persons.Add(new Person() {FirstName = "P1", LastName = "L1"});\n        Persons.Add(new Person() {FirstName = "P2", LastName = "L2"});\n        Persons.Add(new Person() {FirstName = "P3", LastName = "L3"});\n    }\n\n    public ObservableCollection<Person> Persons { get; private set; }\n\n    public ICollectionView MyView { get; private set; } \n\n    public event PropertyChangedEventHandler PropertyChanged;\n\n    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)\n    {\n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n    }\n}\n\npublic class Person : INotifyPropertyChanged\n{\n    private string _firstName;\n    private string _lastName;\n\n    public string FirstName\n    {\n        get { return _firstName; }\n        set\n        {\n            _firstName = value; \n            OnPropertyChanged();\n        }\n    }\n\n    public string LastName\n    {\n        get { return _lastName; }\n        set\n        {\n            _lastName = value;\n            OnPropertyChanged();\n        }\n    }\n\n    public event PropertyChangedEventHandler PropertyChanged;\n\n    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)\n    {\n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n    }\n}\n\npublic class TestBehavior : Behavior<DataGrid>\n{\n    protected override void OnAttached()\n    {\n        base.OnAttached();\n        AssociatedObject.Unloaded += AssociatedObjectUnloaded;\n    }\n\n    private void AssociatedObjectUnloaded(object sender, RoutedEventArgs e)\n    {\n        //look at this in Debug Mode, its NULL if you dont use the TemplateSelector\n        var itemssource = AssociatedObject.ItemsSource;\n\n\n    }\n\n    protected override void OnDetaching()\n    {\n        base.OnDetaching();\n        AssociatedObject.Unloaded -= AssociatedObjectUnloaded;\n    }\n}\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

MyGridControl.xaml

\n\n
<UserControl x:Class="DataGridICollectionView.MyGridControl"\n         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"\n         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"\n         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" \n         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" \n         xmlns:local="clr-namespace:DataGridICollectionView"\n         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"\n         mc:Ignorable="d" \n         d:DesignHeight="300" d:DesignWidth="300">\n<Grid>\n    <DataGrid ItemsSource="{Binding MyView}" AutoGenerateColumns="True">\n        <i:Interaction.Behaviors>\n            <local:TestBehavior/>\n        </i:Interaction.Behaviors>\n    </DataGrid>\n</Grid>\n</UserControl>\n
Run Code Online (Sandbox Code Playgroud)\n\n

MyViewUc.xaml

\n\n
<UserControl x:Class="DataGridICollectionView.MyViewUc"\n         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"\n         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"\n         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" \n         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" \n         xmlns:local="clr-namespace:DataGridICollectionView"\n         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"\n         mc:Ignorable="d" \n         d:DesignHeight="300" d:DesignWidth="300">\n<UserControl.Resources>\n    <DataTemplate x:Key="MyViewCrap">\n        <local:MyGridControl/>\n    </DataTemplate>\n\n    <local:MyTemplateSelector x:Key="Selector" GridView="{StaticResource MyViewCrap}" />\n</UserControl.Resources>\n<Grid>\n    <!--When using Contentcontrol with TemplateSelector- ItemsSource is NOT set to null -->\n    <ContentControl Content="{Binding .}" ContentTemplateSelector="{StaticResource Selector}"/>\n    <!--When using MyGridControl withOUT TemplateSelector- ItemsSource is set to NULL -->\n    <!--<local:MyGridControl/>-->\n</Grid>\n</UserControl>\n
Run Code Online (Sandbox Code Playgroud)\n\n

MyViewUc.xaml.cs

\n\n
namespace DataGridICollectionView\n{\n/// <summary>\n/// Interaktionslogik f\xc3\xbcr MyViewUc.xaml\n/// </summary>\npublic partial class MyViewUc : UserControl\n{\n    public MyViewUc()\n    {\n        InitializeComponent();\n    }\n}\n\npublic class MyTemplateSelector : DataTemplateSelector\n{\n    public DataTemplate GridView { get; set; }\n\n\n    public override DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)\n    {\n        var chooser = item as ViewmodelListe;\n        if (chooser == null)\n        {\n            return base.SelectTemplate(item, container);\n        }\n\n        return GridView;\n    }\n}\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

编辑:我最终使用这个

\n\n
public class MyDataGrid : DataGrid\n{\n\n    static MyDataGrid ()\n    {\n        ItemsSourceProperty.OverrideMetadata(typeof(MyDataGrid ),new FrameworkPropertyMetadata(null, OnPropertyChangedCallBack, OnCoerceItemsSourceProperty));\n    }\n\n    private ICollectionView _defaultView;\n    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)\n    {\n        if(_defaultView != null)\n            _defaultView.CollectionChanged -= LiveSortingPropertiesOnCollectionChanged;\n\n        base.OnItemsSourceChanged(oldValue, newValue);\n\n        _defaultView = newValue as ICollectionView;\n        if(_defaultView != null)\n            _defaultView.CollectionChanged += LiveSortingPropertiesOnCollectionChanged;\n    }\n\n    private void LiveSortingPropertiesOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)\n    {\n        if (e.Action == NotifyCollectionChangedAction.Reset)\n        {\n            foreach (var dataGridColumn in this.Columns)\n            {\n                var isSortDirectionSetFromCollectionView = false;\n                foreach (var sortDescription in _defaultView.SortDescriptions)\n                {\n                    if (dataGridColumn.SortMemberPath == sortDescription.PropertyName)\n                    {\n                        dataGridColumn.SortDirection = sortDescription.Direction;\n                        isSortDirectionSetFromCollectionView = true;\n                        break;\n                    }\n                }\n\n                if (!isSortDirectionSetFromCollectionView)\n                {\n                    dataGridColumn.SortDirection = null;\n                }\n            }\n        }\n    }\n\n    private static void OnPropertyChangedCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)\n    {\n        var grd = d as MyDataGrid ;\n        var view = e.NewValue as ICollectionView;\n\n        if (grd == null || view == null)\n            return;\n\n        foreach (var dataGridColumn in grd.Columns)\n        {\n            var isSortDirectionSetFromCollectionView = false;\n            foreach (var sortDescription in view.SortDescriptions)\n            {\n                if (dataGridColumn.SortMemberPath == sortDescription.PropertyName)\n                {\n                    dataGridColumn.SortDirection = sortDescription.Direction;\n                    isSortDirectionSetFromCollectionView = true;\n                    break;\n                }\n            }\n            //wenn die View nicht sortiert war, auch die column nicht Sortieren\n            if (!isSortDirectionSetFromCollectionView)\n            {\n                dataGridColumn.SortDirection = null;\n            }\n        }\n    }\n\n\n    private static object OnCoerceItemsSourceProperty(DependencyObject d, object baseValue)\n    {\n        // do nothing here - we just want to override parent behaviour.\n        // The _only_ thing DataGrid does here is clearing sort descriptors\n        return baseValue;\n    }\n\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Evk*_*Evk 5

当您直接在 MyViewUc 中托管 MyGridControl 时(情况 1) - 当您切换工作区并且 MyViewUC 被卸载时,它的数据上下文设置为 null。因为 MyGridControl 是直接子级 - 它的 datacontext 也设置为 null,而 DataGrid 的 DataContext 也设置为 null。这也将 ItemsSource 设置为 null,因为它绑定到 DataContext。您可以通过查看行为中 DataGrid 的 DataContext 来验证这一点。这种行为在我看来是完全合理的。

当您使用模板选择器时:MyViewUC 被卸载,它的 datacontext 设置为 null。然后 ContentControl Content 也设置为 null。现在问题是:当您使用 ContentTemplateSelector 时,旧的(已卸载的)MyGridControl 的 DataContext 未设置为 null。您可以在您的行为中验证这一点,这就是保留 ItemsSource 和排序描述符的原因。

现在,我认为第二种行为是不正确的,对于由 ContentTemplateSelector 创建的此卸载控件,应将 datacontext 设置为 null。这背后的逻辑不是很简单 - 您可以自己查看 ContentPresenter.OnContentChanged 方法的源代码,您将看到内容更改时 DataContext 何时不更新。

更新:我看到您主要担心的是丢失排序描述符,但这是丢失 DataContext 并将 ItemsSource 设置为 null 的直接后果。对我来说,这种行为看起来很合理,但我确实发现对于很多人来说事实并非如此,因此甚至有关于此问题的错误报告: https: //connect.microsoft.com/VisualStudio/feedback/details/592897/collectionviewsource-sorting -仅第一次绑定到源

您可以在 DataGrid 源代码中看到:

protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
  base.OnItemsSourceChanged(oldValue, newValue);
  if (newValue == null)
    this.ClearSortDescriptionsOnItemsSourceChange();
  // more code here....
}
Run Code Online (Sandbox Code Playgroud)

因此,当您将 ItemsSource 设置为 null 时,所有排序描述符都会被显式清除。在上面的链接中,您可以找到一些可能有用的解决方法。

UPDATE2:您可以考虑尝试通过继承 DataGrid 来修复该行为。我并不是说这是完美的解决方案,但使用 ContentTemplateSelector 也不是。当 ItemsSource 设置为 null 时,有两个地方会清除排序描述符 - OnItemsSourceChanged 和 OnCoerceItemsSourceProperty。因此您可以执行以下操作:

public class MyDataGrid : DataGrid {
    static MyDataGrid() {
        ItemsSourceProperty.OverrideMetadata(typeof(MyDataGrid), new FrameworkPropertyMetadata(null, OnCoerceItemsSourceProperty));
    }

    private static object OnCoerceItemsSourceProperty(DependencyObject d, object baseValue) {
        // do nothing here - we just want to override parent behaviour.
        // The _only_ thing DataGrid does here is clearing sort descriptors
        return baseValue;
    }

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) {
        SortDescription[] sorts = null;
        if (newValue == null) {
            // preserve sort descriptors when setting ItemsSource to null
            sorts = Items.SortDescriptions.ToArray();
        }
        // they will now be cleared here
        base.OnItemsSourceChanged(oldValue, newValue);            
        if (sorts != null) {
            // restore them back
            foreach (var sort in sorts) {
                Items.SortDescriptions.Add(sort);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

通过上面的代码,您将看到排序描述符在切换数据上下文之间保留在 MyView 中。