AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
24.55k stars 2.12k forks source link

DataGridTemplateColumn with Flyout incorrect contents after sorting #13346

Open stogle opened 8 months ago

stogle commented 8 months ago

Describe the bug

In a DataGrid with a DataGridTemplateColumn whose CellTemplate contains a Flyout, after sorting the DataGrid, the Flyouts no longer correspond to the correct rows in the grid.

To Reproduce

Create a new Avalonia C# project targeting Desktop, using Community Toolkit, with Compiled Bindings, then make the following modifications:

MainView.axaml:

<UserControl 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"
             xmlns:vm="clr-namespace:DataGridBug.ViewModels"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="DataGridBug.Views.MainView"
             x:DataType="vm:MainViewModel"
             Design.DataContext="vm:MainViewModel">
  <DataGrid ItemsSource="{Binding Alarms}">
    <DataGrid.Columns>
      <DataGridTextColumn Binding="{Binding Description}"
                          Header="Description" />

      <DataGridTemplateColumn Header="State"
                              SortMemberPath="State">
        <DataGridTemplateColumn.CellTemplate>
          <DataTemplate DataType="vm:AlarmViewModel">
            <Button Content="{Binding State}">
              <Button.Flyout>
                <MenuFlyout ItemsSource="{Binding MenuItems}">
                    <MenuFlyout.ItemContainerTheme>
                        <ControlTheme x:DataType="vm:MenuItemViewModel"
                                      BasedOn="{StaticResource {x:Type MenuItem}}"
                                      TargetType="MenuItem">
                            <Setter Property="Command" Value="{Binding Command}" />
                            <Setter Property="CommandParameter" Value="{Binding CommandParameter}" />
                            <Setter Property="Header" Value="{Binding Header}" />
                        </ControlTheme>
                    </MenuFlyout.ItemContainerTheme>
                </MenuFlyout>
              </Button.Flyout>
            </Button>
          </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
      </DataGridTemplateColumn>
    </DataGrid.Columns>
  </DataGrid>
</UserControl>

MainViewModel.cs:

public partial class MainViewModel : ViewModelBase
{
    [ObservableProperty] private IReadOnlyList<AlarmViewModel> _alarms = new[]
    {
        new AlarmViewModel("Alarm 1", "On"),
        new AlarmViewModel("Alarm 2", "Off"),
        new AlarmViewModel("Alarm 3", "On"),
        new AlarmViewModel("Alarm 4", "Off"),
        new AlarmViewModel("Alarm 5", "On"),
        new AlarmViewModel("Alarm 6", "Off"),
        new AlarmViewModel("Alarm 7", "On"),
        new AlarmViewModel("Alarm 8", "Off")
    };
}

public partial class AlarmViewModel : ViewModelBase
{
    public AlarmViewModel(string description, string state)
    {
        Description = description;
        State = state;
        MenuItems = new[]
        {
            new MenuItemViewModel { Command = SetStateCommand, CommandParameter = "On", Header = $"Turn on {Description}" },
            new MenuItemViewModel { Command = SetStateCommand, CommandParameter = "Off", Header = $"Turn off {Description}" }
        };
    }

    [ObservableProperty] private string? _description;
    [ObservableProperty] private string? _state;
    [ObservableProperty] private IReadOnlyList<MenuItemViewModel> _menuItems;
    [RelayCommand] private void SetState(string value) => State = value;
}

public partial class MenuItemViewModel : ViewModelBase
{
    [ObservableProperty] private ICommand? _command;
    [ObservableProperty] private object? _commandParameter;
    [ObservableProperty] private object? _header;
}
  1. Run the application
  2. Click on each of the buttons in the State column and observe that the menu items in the MenuFlyout are correct for each row (i.e. the row with description "Alarm 1" has menu items "Turn on Alarm 1" and "Turn off Alarm 1", etc.).
  3. Click on the State header to sort the DataGrid in ascending order by State.
  4. Click on each of the buttons in the State column and observe that the menu items in the MenuFlyout are incorrect for some rows (e.g. the row with description "Alarm 1" may have menu items "Turn on Alarm 4" and "Turn off Alarm 4").

Expected behavior

The contents of MenuFlyouts should always be correct for each row, even after sorting.

Screenshots

DataGridBug Recording 2023-10-22 152358

Environment

Additional context

I also tried removing the ItemContainerTheme and having the ItemsSource be an array of MenuItem controls, instead of MenuItemViewModel. This was even worse, leading to an exception (System.InvalidOperationException: 'The control already has a visual parent.') when clicking on some buttons after sorting.

I'm guessing this has something to do with container recycling, but I don't know much about how that works or even if it is possible to turn it off.

Developer-Alexander commented 8 months ago

I also made the experience that sorting of a DataGrid does not work properly. It looked like that not all columns were sorted.