monstr
monstr

Reputation: 1730

Sorting ObservableCollection with ICollectionView does't work correctly

To generate the bug, select any item in TopDataGrid. As a result, the collection of items will be loaded into BottomDataGrid. This collection is sorted by Name property as I specified! Then select any other item in the TopDataGrid. The result is that ItemsSource of BottomDataGrid will be reloaded. And now the collection is unsorted! The collection looks as I specified it in code. Moreover, if I check _customerView with the debugger I see the sorted collection.

I know I can use List with OrderBy and INotifyPropertyChanged to explicitly command the UI to update itself instead of ObservableCollection and ICollectionView. But I think this is not the proper approach.

Win 7, .Net 4.0. Just copy and paste.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <DataGrid Grid.Row="0" 
                AutoGenerateColumns="False"
                ItemsSource="{Binding Items}"
                SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                CanUserAddRows="False"
                CanUserDeleteRows="False"
                CanUserResizeRows="False"
                CanUserSortColumns="False"
                SelectionMode="Single"
                SelectionUnit="FullRow">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding Name}"></DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>
    <DataGrid Grid.Row="1" 
                AutoGenerateColumns="False"
                ItemsSource="{Binding SelectedItem.MyCollectionView}"
                CanUserAddRows="False"
                CanUserDeleteRows="False"
                CanUserResizeRows="False"
                CanUserSortColumns="False"
                SelectionMode="Single"
                SelectionUnit="FullRow">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding Name}"></DataGridTextColumn>
            <DataGridTextColumn Binding="{Binding Index}"></DataGridTextColumn>
        </DataGrid.Columns>

    </DataGrid>

    <Grid Grid.Row="2">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TextBox Grid.Column="0" 
                    Text="{Binding Name1}"></TextBox>
        <TextBox Grid.Column="1" 
                    Text="{Binding Index}"></TextBox>
    </Grid>
</Grid>

code

public class TopGridItem
{
    private ObservableCollection<BottomGridItem> _collection;
    public ObservableCollection<BottomGridItem> Collection
    {
        get { return _collection; }
    }

    public String Name { get; set; }

    public ICollectionView MyCollectionView
    {
        get
        {
            ICollectionView _customerView = CollectionViewSource.GetDefaultView(Collection);
            _customerView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));

            return _customerView;
        }
    }

    public TopGridItem()
    {
        _collection = new ObservableCollection<BottomGridItem>();
        _collection.Add(new BottomGridItem { Name = "bbbbbb" });
        _collection.Add(new BottomGridItem { Name = "aaaaa" });
        _collection.Add(new BottomGridItem { Name = "aaaaa" });
        _collection.Add(new BottomGridItem { Name = "ccccc" });
        _collection.Add(new BottomGridItem { Name = "dddddd" });
    }

}

public class BottomGridItem
{
    public String Name { get; set; }
    public String Index { get; set; }
}

/// <summary>
/// Логика взаимодействия для NewWindow.xaml
/// </summary>
public partial class ProgressWindow : INotifyPropertyChanged
{
    public TopGridItem _selectedItem;

    public String Name1 { get; set; }
    public String Index { get; set; }
    public ObservableCollection<TopGridItem> Items { get; set; }

    public TopGridItem SelectedItem 
    {
        get { return _selectedItem; }

        set
        {
            _selectedItem = value;
            OnPropertyChanged("SelectedItem");

        }
    }

    public ProgressWindow()
    {
        InitializeComponent();
        DataContext = this;

        Items = new ObservableCollection<TopGridItem>();
        Items.Add(new TopGridItem {Name = "One"});
        Items.Add(new TopGridItem {Name = "Two"});
        Items.Add(new TopGridItem {Name = "Three"});
    }

    #region INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion
}

UPDATE

private void ClearSortDescriptionsOnItemsSourceChange()
    {
      this.Items.SortDescriptions.Clear();
      this._sortingStarted = false;
      List<int> descriptionIndices = this.GroupingSortDescriptionIndices;
      if (descriptionIndices != null)
        descriptionIndices.Clear();
      foreach (DataGridColumn dataGridColumn in (Collection<DataGridColumn>) this.Columns)
        dataGridColumn.SortDirection = new ListSortDirection?();
    }

    private static object OnCoerceItemsSourceProperty(DependencyObject d, object baseValue)
    {
      DataGrid dataGrid = (DataGrid) d;
      if (baseValue != dataGrid._cachedItemsSource && dataGrid._cachedItemsSource != null)
        dataGrid.ClearSortDescriptionsOnItemsSourceChange();
      return baseValue;
    }

It seems like in ClearSortDescriptionsOnItemsSourceChange method sorting is cleared and not respecify again. I think this is the matter.

Upvotes: 2

Views: 5754

Answers (2)

Steven Rands
Steven Rands

Reputation: 5421

There is a hacky workaround :O)

Change your TopGridItem class so it implements INotifyPropertyChanged, then change the MyCollectionView property like so:

public ICollectionView MyCollectionView
{
    get
    {
        return _myCollectionView;
    }
    set
    {
        _myCollectionView = value;
        OnPropertyChanged("MyCollectionView");
    }
}
ICollectionView _myCollectionView;

Add a public method to TopGridItem like so:

public void ResetView()
{
    MyCollectionView = null;  // This is the key to making it work
    ICollectionView customerView = CollectionViewSource.GetDefaultView(_collection);
    customerView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
    MyCollectionView = customerView;
}

This requires that _collection be made a class field rather than a variable that is only used in the constructor. Speaking of which:

public TopGridItem()
{
    _collection = new ObservableCollection<BottomGridItem>();
    _collection.Add(new BottomGridItem { Name = "bbbbbb" });
    _collection.Add(new BottomGridItem { Name = "aaaaa" });
    _collection.Add(new BottomGridItem { Name = "aaaaa" });
    _collection.Add(new BottomGridItem { Name = "ccccc" });
    _collection.Add(new BottomGridItem { Name = "dddddd" });

    ResetView();
}

Note the call to ResetView.

Now, all you need to do in the SelectedItem property setter of ProgressWindow is to call that ResetView method when the selection changes:

public TopGridItem SelectedItem 
{
    get { return _selectedItem; }

    set
    {
        _selectedItem = value;
        OnPropertyChanged("SelectedItem");

        if (_selectedItem != null)
        {
            _selectedItem.ResetView();
        }
    }
}

I've tested this here and it seems to work. If it doesn't then I must have made a typo somewhere or maybe missed something out of the code I've posted.


Working version of code

XAML:

<Window x:Class="WpfApplication4.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid Grid.Row="0"
                  AutoGenerateColumns="False"
                  ItemsSource="{Binding Items}"
                  SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  CanUserResizeRows="False"
                  CanUserSortColumns="False"
                  SelectionMode="Single"
                  SelectionUnit="FullRow">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>

        <DataGrid Grid.Row="1"
                  AutoGenerateColumns="False"
                  ItemsSource="{Binding SelectedItem.MyCollectionView}"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  CanUserResizeRows="False"
                  CanUserSortColumns="False"
                  SelectionMode="Single"
                  SelectionUnit="FullRow">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}"></DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Index}"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>

        <Grid Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBox Grid.Column="0"
                     Text="{Binding Name1}"></TextBox>
            <TextBox Grid.Column="1"
                     Text="{Binding Index}"></TextBox>
        </Grid>
    </Grid>
</Window>

Code-behind:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication4
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            Items = new ObservableCollection<TopGridItem>();
            Items.Add(new TopGridItem { Name = "One" });
            Items.Add(new TopGridItem { Name = "Two" });
            Items.Add(new TopGridItem { Name = "Three" });
        }

        public String Name1 { get; set; }
        public String Index { get; set; }
        public ObservableCollection<TopGridItem> Items { get; private set; }

        public TopGridItem SelectedItem
        {
            get { return _selectedItem; }
            set
            {
                _selectedItem = value;
                OnPropertyChanged("SelectedItem");
                if (_selectedItem != null)
                {
                    _selectedItem.ResetView();
                }
            }
        }
        TopGridItem _selectedItem;

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class TopGridItem : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }

        public String Name { get; set; }

        public ICollectionView MyCollectionView
        {
            get
            {
                return _myCollectionView;
            }
            set
            {
                _myCollectionView = value;
                OnPropertyChanged("MyCollectionView");
            }
        }
        ICollectionView _myCollectionView;

        public void ResetView()
        {
            MyCollectionView = null;
            ICollectionView _customerView = CollectionViewSource.GetDefaultView(_collection);
            _customerView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
            MyCollectionView = _customerView;
        }

        ObservableCollection<BottomGridItem> _collection;

        public TopGridItem()
        {
            _collection = new ObservableCollection<BottomGridItem>();
            _collection.Add(new BottomGridItem { Name = "bbbbbb" });
            _collection.Add(new BottomGridItem { Name = "aaaaa" });
            _collection.Add(new BottomGridItem { Name = "aaaaa" });
            _collection.Add(new BottomGridItem { Name = "ccccc" });
            _collection.Add(new BottomGridItem { Name = "dddddd" });
            ResetView();
        }
    }

    public class BottomGridItem
    {
        public String Name { get; set; }
        public String Index { get; set; }
    }
}

Upvotes: 0

monstr
monstr

Reputation: 1730

After some investigations and attempts I think I can suggest at least two solutions.

First:

public TopGridItem SelectedItem 
{
    get { return _selectedItem; }

    set
    {
        _selectedItem = value;
        OnPropertyChanged("SelectedItem");

        // _dataGrid - link to BottomDataGrid  
        _dataGrid.Items.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));

    }
}

Second:

public TopGridItem SelectedItem 
{
    get { return _selectedItem; }

    set
    {
        _selectedItem = value;
        OnPropertyChanged("SelectedItem");

        if (_selectedItem != null)
        _selectedItem.MyCollectionView.Refresh();
    }
}

Upvotes: 3

Related Questions