Avalonia icon indicating copy to clipboard operation
Avalonia copied to clipboard

Datagrid ClearSort doesn't work if CanUserSortColumns subsequently synchronously set to false

Open wsficke opened this issue 1 year ago • 5 comments

Describe the bug Consider the case where the developer wishes to sometimes let the user sort columns by clicking the header, and at other times disable manual sorting. The developer wants to write code to clear the user-provided sorts for all columns, then disable the user from providing further sorts. In this case, the idiomatic way is to iterate the columns, clearing the sort for each, then set the CanUserSortColumns to false. However, it doesn't work quite right.

To Reproduce Steps to reproduce the behavior:

Front End

<Window xmlns="https://github.com/avaloniaui"
        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"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="DataGridTest.MainWindow"
        Title="DataGridTest">
	<StackPanel>
		<Button Content="Clear Sort" Click="OnClearSortClick"></Button>
		<DataGrid x:Name="DataGrid" Items="{Binding Persons}">
			<DataGrid.Columns>
				<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
				<DataGridTextColumn Header="Age" Binding="{Binding Age}" />
			</DataGrid.Columns>
		</DataGrid>
	</StackPanel>
</Window>

Code Behind

using Avalonia.Controls;
using Avalonia.Interactivity;
using System.Collections.ObjectModel;

namespace DataGridTest;

public partial class MainWindow : Window
{
    public ObservableCollection<Person> Persons { get; } = new ObservableCollection<Person>
    {
        new Person { Name = "John Doe", Age = 30 },
        new Person { Name = "Jane Doe", Age = 25 },
        // ...
    };

    public MainWindow()
    {
        DataContext = this;
        InitializeComponent();
    }

    private void OnClearSortClick(object sender, RoutedEventArgs e)
    {
        foreach (var column in DataGrid.Columns)
        {
            column.ClearSort();
        }
        DataGrid.CanUserSortColumns = false;
    }

    public sealed class Person
    {
        public string Name { get; set; } = string.Empty;
        public int Age { get; set; }
    }
}
  1. Launch the app
  2. Click a header to set a sort
  3. Click the clear sort button

Expected behavior Column sorts are cleared and the user cannot provide any futher sorts

Observed behavior Column sorts are not cleared and the user cannot provide any further sorts

Workaround Post a callback to the main thread dispatcher instead of setting CanUserSortColumns synchronously.

    foreach (var column in DataGrid.Columns)
    {
        column.ClearSort();
    }
    Dispatcher.UIThread.Post(() => DataGrid.CanUserSortColumns = false);

Screenshots run the reproducible example

Desktop (please complete the following information):

  • OS: Windows 11
  • Version: 0.10.21

Additional context Add any other context about the problem here. DataGridTest.zip

wsficke avatar Jun 30 '23 15:06 wsficke

I have another workaround without using the dispatcher. It requires the wrapper I mentioned in this discussion, and with a few changes it works as expected:

public DataGridItemCollectionView<Person> Persons { get; } = new(new List<Person>
{
    new() { Name = "John Doe", Age = 30 },
    new() { Name = "Jane Doe", Age = 25 },
});

public DataGridCollectionView DataGridView => Persons;

public void OnClearSortClick(object? sender, RoutedEventArgs e)
{
    DataGridView.SortDescriptions.Clear();
}

public sealed class Person : PropertyChangedBase
{
    public string Name { get; set; } = string.Empty;

    public int Age { get; set; }
}

INotifyPropertyChanged on Person is only required if you also implement INotifyCollectionChanged on DataGridItemCollectionView<T>. This way the sorting can also be modified and cleared from the view model, not code behind.

VMelnalksnis avatar Jul 02 '23 06:07 VMelnalksnis

I have another workaround without using the dispatcher. It requires the wrapper I mentioned in https://github.com/AvaloniaUI/Avalonia/discussions/11907#discussioncomment-6300413, and with a few changes it works as expected:

@VMelnalksnis I am having difficulty replicating your result. Looking at your code, it seems that it only clears the sort order, but does not subsequently and synchronously disable further user-provided sorts. The defect only occurs in the subsequently/synchronously scenario, so based on just clearing the sort orders we should expect no unusual result. After reading your wrapper from the other thread, I am left with the following code-behind:

using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Interactivity;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace DataGridTest;

public sealed class DataGridItemCollectionView<T> : IEnumerable<T>
{
    private readonly DataGridCollectionView _dataGridCollectionView;

    public DataGridItemCollectionView(IEnumerable<T> source)
        : this(new DataGridCollectionView(source))
    {
    }

    public DataGridItemCollectionView(DataGridCollectionView dataGridCollectionView)
    {
        _dataGridCollectionView = dataGridCollectionView;
    }

    public static implicit operator DataGridCollectionView(DataGridItemCollectionView<T> view) =>
        view._dataGridCollectionView;

    public IEnumerator<T> GetEnumerator() => _dataGridCollectionView.Cast<T>().GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => _dataGridCollectionView.GetEnumerator();
}

public sealed class Person : INotifyPropertyChanged
{
    private string name = string.Empty;
    private int age;

    public string Name
    {
        get => name;
        set
        {
            if (value == name) return;
            name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }

    public int Age
    {
        get => age;
        set
        {
            if (value == age) return;
            age = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Age)));
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

public partial class MainWindow : Window
{
    public DataGridItemCollectionView<Person> Persons { get; } = new(new List<Person>
    {
        new() { Name = "John Doe", Age = 30 },
        new() { Name = "Jane Doe", Age = 25 },
    });

    public DataGridCollectionView DataGridView => Persons;

    public void OnClearSortClick(object? sender, RoutedEventArgs e)
    {
        DataGridView.SortDescriptions.Clear();
    }
}

Unfortunately, this doesn't even show the DataGrid. Can you please zip up your working isolation test and attach in reply?

wsficke avatar Jul 03 '23 13:07 wsficke

@wsficke I forgot to add DataGrid.CanUserSortColumns = false; in the above example. Using this code, after sorting a column and clicking the button, the sorting is reset and cannot be sorted by the user again.

public void OnClearSortClick(object? sender, RoutedEventArgs e)
{
    DataGridView.SortDescriptions.Clear();
    DataGrid.CanUserSortColumns = false;
}

I'll try to extract all the relevant code a bit later.

VMelnalksnis avatar Jul 03 '23 14:07 VMelnalksnis

@VMelnalksnis best if you can file a minimal sample showing your issue.

timunie avatar Jul 04 '23 07:07 timunie

Here's examples with DataGridCollectionView wrapper where this works as expected, both in code-behind and with a view model.

SortClearTest.zip SortClearTest_ViewModel.zip

VMelnalksnis avatar Jul 04 '23 08:07 VMelnalksnis