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
26.05k stars 2.25k forks source link

The TreeView seems to have an adverse effect on command binding #17387

Closed chenjing1294 closed 2 weeks ago

chenjing1294 commented 3 weeks ago

Describe the bug

When I upgraded from 11.1.4 to 11.2.0, the command binding on the button failed. The following sample code can reproduce the problem.

11.1.4 good

https://github.com/user-attachments/assets/2a734bd3-f9a1-436b-b817-62a3cccd9b02

11.2.0 Have a problem

When "a" is selected, the button is still unavailable

https://github.com/user-attachments/assets/c2ae6499-7199-4672-87e6-ff1a76c541e1

To Reproduce

The following sample code can reproduce the problem.

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:windows="clr-namespace:AvaloniaApplication1"
        x:Class="AvaloniaApplication1.MainWindow"
        Title="Avalonia Quickstart" Width="600" Height="450"
        x:DataType="windows:MainViewModel">
  <Window.DataContext>
    <windows:MainViewModel />
  </Window.DataContext>
  <Grid ColumnDefinitions="1*,Auto">
    <TreeView Grid.Column="0"
              SelectedItem="{Binding SelectedConnectionViewModel, Mode=TwoWay}"
              ItemsSource="{Binding ConnectionViewModels, Mode=OneWay}">
      <TreeView.ItemTemplate>
        <DataTemplate DataType="windows:ConnectionViewModel">
          <TextBlock Text="{Binding Title, Mode=OneWay}" />
        </DataTemplate>
      </TreeView.ItemTemplate>
    </TreeView>
    <Button Grid.Column="1" Content="ClickMe"
            Command="{Binding CreateSlave, Mode=OneWay}">
      <Button.CommandParameter>
        <MultiBinding Mode="OneWay">
          <Binding Path="SelectedConnectionViewModel" Mode="OneWay" />
          <Binding Path="SelectedConnectionViewModel.IsOpened" Mode="OneWay" />
        </MultiBinding>
      </Button.CommandParameter>
    </Button>
  </Grid>
</Window>
using System.Collections.ObjectModel;
using Avalonia.Controls;

namespace AvaloniaApplication1;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        if (this.DataContext is MainViewModel mainViewModel)
        {
            mainViewModel.ConnectionViewModels.Add(new ConnectionViewModel()
            {
                Title = "a",
                IsOpened = false,
            });
            mainViewModel.ConnectionViewModels.Add(new ConnectionViewModel()
            {
                Title = "b",
                IsOpened = true,
            });
        }
    }
}

public class MainViewModel : ViewModelBase
{
    public ObservableCollection<ConnectionViewModel> ConnectionViewModels { get; } = new ObservableCollection<ConnectionViewModel>();
    private ConnectionViewModel _selectedConnectionViewModel;

    public ConnectionViewModel SelectedConnectionViewModel
    {
        get => _selectedConnectionViewModel;
        set => SetField(ref _selectedConnectionViewModel, value, nameof(SelectedConnectionViewModel));
    }

    public void CreateSlave(object parameter)
    {
        if (parameter is ReadOnlyCollection<object> args && args[0] is ConnectionViewModel connectionViewModel && args[1] is bool opened)
        {
        }
    }

    public bool CanCreateSlave(object parameter)
    {
        if (parameter is ReadOnlyCollection<object> args && args[0] is ConnectionViewModel _ && args[1] is bool opened)
        {
            return !opened;
        }

        return false;
    }
}

public class ConnectionViewModel : ViewModelBase
{
    private string _title;

    public string Title
    {
        get => _title;
        set => SetField(ref _title, value, nameof(Title));
    }

    private bool _isOpened;

    public bool IsOpened
    {
        get => _isOpened;
        set => SetField(ref _isOpened, value, nameof(_isOpened));
    }
}

Expected behavior

No response

Avalonia version

11.2.0

OS

No response

Additional context

No response

rabbitism commented 3 weeks ago

Should you add a converter in multibinding?

IanRawley commented 3 weeks ago

At a guess a MultiBinding does not produce a ReadOnlyCollection or anything that inherits from that, so your CanCreateSlave() method is always returning false.

chenjing1294 commented 3 weeks ago

@rabbitism @IanRawley If you don't add a Converter, the ReadOnlyCollection is passed when the CanCreateSlave method is called. The problem is that in 11.1.4, when I select different TreeItems, the CanCreateSlave method is called, but in 11.2.0, the CanCreateSlave method is never called.

IanRawley commented 3 weeks ago

You need to annotate your CanExecute equivalent method with the property or properties that can trigger changes. See: https://docs.avaloniaui.net/docs/guides/data-binding/how-to-bind-to-a-command-without-reactiveui#trigger-can-execute

It seems MultiBindings don't automatically trigger re-evaluations of CanExecute for method bound commands. Binding to a single Property does though. So the problem isn't TreeView, it's MultiBinding.

chenjing1294 commented 3 weeks ago

I now understand that it is not a problem with TreeView, but a problem with MultiBinding or Command. Even if I don't use annotation, the Can method should be triggered because the properties have changed.

IanRawley commented 2 weeks ago

I've figured out the problem here. When a MultiBinding doesn't have a converter, the associated MultiBindingExpressions simply publishes a wrapper ReadOnlyCollection around its internal values array. The collection itself isn't Observable, and never actually itself changes, so while the values contained have changed the list itself is the same. As a result, the Binding system just silently ignores it because from its point of view nothing has changed.

IanRawley commented 2 weeks ago

This was introduced by #16219 Previously a MultiBinding would instantiate a new collection of values every time it needed to publish a value without a converter. The new MultiBindingExpression instead reuses the same ReadOnlyCollection, and so will effectively only ever publish a value once.

chenjing1294 commented 2 weeks ago

@IanRawley I understand. Thank you very much for your answer.