Datagrid с ICollectionView SortDescription потерялся — ошибка?

вот что я хочу: если я привяжу ICollectionview к DataGrid, я не хочу потерять SortDescription в моей модели представления.

я создаю небольшой пример проекта, чтобы увидеть, что я имею в виду. В своих проектах я просто использую Usercontrol для отображения своих данных в DataGrid. Если я сделаю это, SortDescritpion исчезнет при выгрузке UserControl, поскольку для ItemsSource установлено значение null. Если я использую TemplateSelector для отображения своего UserControl, SortDescription не исчезнет, а для ItemsSource не будет установлено значение null при выгрузке. вопрос в том, почему такое разное поведение? Является ли одно из двух действий ошибкой?

Кстати. Я использую .Net 4.5.1, но установлена ​​4.6.1 и system.Windows.Interactivity 4.0.0.0

MainWindow.xaml

<Window x:Class="DataGridICollectionView.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:DataGridICollectionView"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <DataTemplate DataType="{x:Type local:ViewmodelListe}">
        <local:MyViewUc/>
    </DataTemplate>
</Window.Resources>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <ToolBar Grid.Row="0">
        <Button Content="SetWorkspace MyView" Click="Button_Click"/>
        <Button Content="SetWorkspace Other" Click="Button_Click_1"/>
    </ToolBar>

    <ContentPresenter Grid.Row="1" Content="{Binding Workspace}"/>
</Grid>
</Window>

MainWindow.xaml.cs

namespace DataGridICollectionView
{
/// <summary>
/// Interaktionslogik für MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
    private object _workspace;

    public MainWindow()
    {
        InitializeComponent();
        MyViewVm = new ViewmodelListe();

        DataContext = this;
    }

    public ViewmodelListe MyViewVm { get; set; }

    public object Workspace
    {
        get { return _workspace; }
        set
        {
            _workspace = value;
            OnPropertyChanged();
        }
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Workspace = MyViewVm;
    }

    private void Button_Click_1(object sender, RoutedEventArgs e)
    {
        Workspace = "Other";
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class ViewmodelListe : INotifyPropertyChanged
{
    public ViewmodelListe()
    {
        Persons = new ObservableCollection<Person>();
        MyView = CollectionViewSource.GetDefaultView(Persons);

        Persons.Add(new Person() {FirstName = "P1", LastName = "L1"});
        Persons.Add(new Person() {FirstName = "P2", LastName = "L2"});
        Persons.Add(new Person() {FirstName = "P3", LastName = "L3"});
    }

    public ObservableCollection<Person> Persons { get; private set; }

    public ICollectionView MyView { get; private set; } 

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class Person : INotifyPropertyChanged
{
    private string _firstName;
    private string _lastName;

    public string FirstName
    {
        get { return _firstName; }
        set
        {
            _firstName = value; 
            OnPropertyChanged();
        }
    }

    public string LastName
    {
        get { return _lastName; }
        set
        {
            _lastName = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class TestBehavior : Behavior<DataGrid>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Unloaded += AssociatedObjectUnloaded;
    }

    private void AssociatedObjectUnloaded(object sender, RoutedEventArgs e)
    {
        //look at this in Debug Mode, its NULL if you dont use the TemplateSelector
        var itemssource = AssociatedObject.ItemsSource;


    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.Unloaded -= AssociatedObjectUnloaded;
    }
}
}

Мигридконтрол.xaml

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

MyViewUc.xaml

<UserControl x:Class="DataGridICollectionView.MyViewUc"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:DataGridICollectionView"
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
    <DataTemplate x:Key="MyViewCrap">
        <local:MyGridControl/>
    </DataTemplate>

    <local:MyTemplateSelector x:Key="Selector" GridView="{StaticResource MyViewCrap}" />
</UserControl.Resources>
<Grid>
    <!--When using Contentcontrol with TemplateSelector- ItemsSource is NOT set to null -->
    <ContentControl Content="{Binding .}" ContentTemplateSelector="{StaticResource Selector}"/>
    <!--When using MyGridControl withOUT TemplateSelector- ItemsSource is set to NULL -->
    <!--<local:MyGridControl/>-->
</Grid>
</UserControl>

MyViewUc.xaml.cs

namespace DataGridICollectionView
{
/// <summary>
/// Interaktionslogik für MyViewUc.xaml
/// </summary>
public partial class MyViewUc : UserControl
{
    public MyViewUc()
    {
        InitializeComponent();
    }
}

public class MyTemplateSelector : DataTemplateSelector
{
    public DataTemplate GridView { get; set; }


    public override DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
    {
        var chooser = item as ViewmodelListe;
        if (chooser == null)
        {
            return base.SelectTemplate(item, container);
        }

        return GridView;
    }
}
}

РЕДАКТИРОВАТЬ: в итоге я использую это

public class MyDataGrid : DataGrid
{

    static MyDataGrid ()
    {
        ItemsSourceProperty.OverrideMetadata(typeof(MyDataGrid ),new FrameworkPropertyMetadata(null, OnPropertyChangedCallBack, OnCoerceItemsSourceProperty));
    }

    private ICollectionView _defaultView;
    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        if(_defaultView != null)
            _defaultView.CollectionChanged -= LiveSortingPropertiesOnCollectionChanged;

        base.OnItemsSourceChanged(oldValue, newValue);

        _defaultView = newValue as ICollectionView;
        if(_defaultView != null)
            _defaultView.CollectionChanged += LiveSortingPropertiesOnCollectionChanged;
    }

    private void LiveSortingPropertiesOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Reset)
        {
            foreach (var dataGridColumn in this.Columns)
            {
                var isSortDirectionSetFromCollectionView = false;
                foreach (var sortDescription in _defaultView.SortDescriptions)
                {
                    if (dataGridColumn.SortMemberPath == sortDescription.PropertyName)
                    {
                        dataGridColumn.SortDirection = sortDescription.Direction;
                        isSortDirectionSetFromCollectionView = true;
                        break;
                    }
                }

                if (!isSortDirectionSetFromCollectionView)
                {
                    dataGridColumn.SortDirection = null;
                }
            }
        }
    }

    private static void OnPropertyChangedCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var grd = d as MyDataGrid ;
        var view = e.NewValue as ICollectionView;

        if (grd == null || view == null)
            return;

        foreach (var dataGridColumn in grd.Columns)
        {
            var isSortDirectionSetFromCollectionView = false;
            foreach (var sortDescription in view.SortDescriptions)
            {
                if (dataGridColumn.SortMemberPath == sortDescription.PropertyName)
                {
                    dataGridColumn.SortDirection = sortDescription.Direction;
                    isSortDirectionSetFromCollectionView = true;
                    break;
                }
            }
            //wenn die View nicht sortiert war, auch die column nicht Sortieren
            if (!isSortDirectionSetFromCollectionView)
            {
                dataGridColumn.SortDirection = null;
            }
        }
    }


    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;
    }


}

person blindmeis    schedule 24.03.2016    source источник


Ответы (1)


Когда вы размещаете свой MyGridControl непосредственно внутри MyViewUc (случай 1) — когда вы переключаете рабочее пространство и MyViewUC выгружается, для его контекста данных устанавливается значение null. Поскольку MyGridControl является прямым потомком, его контекст данных также имеет значение null, и, в свою очередь, DataContext DataGrid. Это также устанавливает для ItemsSource значение null, поскольку оно привязано к DataContext. Вы можете убедиться в этом, взглянув на DataContext of DataGrid в своем поведении. Такое поведение, на мой взгляд, совершенно разумно.

Когда вы используете селектор шаблонов: MyViewUC выгружается, его контекст данных устанавливается равным нулю. Затем для содержимого ContentControl также устанавливается значение null. Теперь вот проблема: когда вы используете ContentTemplateSelector, DataContext вашего старого (выгруженного) MyGridControl НЕ имеет значение null. Вы можете убедиться в этом по своему поведению, поэтому дескрипторы ItemsSource и sort сохраняются.

Теперь я считаю, что это второе поведение неверно, и для этого выгруженного элемента управления, созданного ContentTemplateSelector, для datacontext должно быть установлено значение null. Логика этого не очень проста — вы можете сами посмотреть исходный код метода ContentPresenter.OnContentChanged, где вы увидите, когда DataContext не обновляется при изменении контента.

ОБНОВЛЕНИЕ: я вижу, что ваша главная проблема связана с потерей дескрипторов сортировки, но это прямое следствие потери DataContext и установки для ItemsSource значения null. Мне такое поведение кажется разумным, но я вижу, что для многих людей это действительно так, поэтому есть даже отчет об ошибке по этой проблеме: https://connect.microsoft.com/VisualStudio/feedback/details/592897/collectionviewsource-sorting-only-the-first-time-it-is-bound-to-a-source

Вы можете увидеть себя в исходном коде DataGrid, который:

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

Поэтому, когда вы устанавливаете для 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);
            }
        }
    }
}

В приведенном выше коде вы увидите, что дескрипторы сортировки сохраняются в вашем MyView между переключением контекста данных.

person Evk    schedule 29.03.2016
comment
это нормально, что DataContext имеет значение NULL, но почему, черт возьми, мой SortDescritpion исчез для моего ICollectionView в моей модели представления? на самом деле это моя проблема :( я не могу представить, что это нормально, что сортировка потеряна только потому, что пользователь переключает контент. - person blindmeis; 30.03.2016
comment
Спасибо. это то, что я ищу, но мне не нравится то, что я вижу :( как вы думаете, обходной путь выбора шаблона имеет какие-то побочные эффекты? - person blindmeis; 30.03.2016
comment
Не уверен насчет побочных эффектов, но я думаю, вы сами видите, что это довольно... странно :) Опубликовал обновление с другой идеей, как это можно решить. - person Evk; 30.03.2016
comment
вы думаете, что это неплохо, и это работает для пользовательского интерфейса, но поскольку я отхожу от своего пользовательского элемента управления сеткой, я теряю описание сортировки в своей модели представления. мне не нравится это состояние :( - person blindmeis; 30.03.2016
comment
Не уверен, что понимаю, что вы имеете в виду, но вы можете попытаться преобразовать oldValue в OnItemsSourceChanged в ICollectionView (потому что это ваш MyView) и сохранить SortDescriptions там, а не в свойстве DataGrid.Items. - person Evk; 30.03.2016
comment
я имею в виду, что если вы просто посмотрите на модель представления, то SortDescription MyView исчезнет, ​​​​если вы отойдете от GridUserControl, и снова установите его через свой код, если вы вернетесь к GridUserControl. но время между ними просто исчезло из моей Viewmodel. - person blindmeis; 30.03.2016
comment
Я не вижу этого. Если я нажму кнопку первой рабочей области, отсортирую по LastName, а затем поставлю точки останова в обработчики событий нажатия кнопки - ваша модель просмотра всегда будет иметь один SortDescriptor, даже если вы сейчас находитесь в другой рабочей области. - person Evk; 30.03.2016
comment
Давайте продолжим это обсуждение в чате. - person Evk; 30.03.2016
comment
@metoyou, это странно, я не вижу общего поведения в обновлении 2. Как именно оно падает? - person Evk; 14.06.2018