Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ListBox selection state not updating correctly #18048

Open
skataben opened this issue Jan 24, 2025 · 6 comments
Open

ListBox selection state not updating correctly #18048

skataben opened this issue Jan 24, 2025 · 6 comments
Labels

Comments

@skataben
Copy link

skataben commented Jan 24, 2025

Describe the bug

When the user interacts with the ListBox to change the selection state of one item and needs that change to select or deselect another item in the ListBox, changes to the view model do not always propagate to the view. Selecting the item a second time does successfully propagate that change.

To Reproduce

Download and run the attached solution.

Details:
MainWindow:
Contains the ListBox item in question.

<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:AvaloniaListBoxMultiselectToggle.ViewModels"
             xmlns:models="clr-namespace:AvaloniaListBoxMultiselectToggle.Models"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:DataType="vm:MainViewModel"
             x:Class="AvaloniaListBoxMultiselectToggle.Views.MainView">
    <Design.DataContext>
        <vm:MainViewModel />
    </Design.DataContext>

    <StackPanel Spacing="10" VerticalAlignment="Center" HorizontalAlignment="Center">

        <TextBlock Text="To repro the bug, select Item1 and then All." FontSize="22" Margin="0,22" />

        <ListBox
            ItemsSource="{Binding Items}"
            SelectionMode="Multiple,Toggle"
            HorizontalAlignment="Center">

            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>

            <ListBox.ItemTemplate>
                <DataTemplate x:DataType="models:OptionItem">
                    <TextBlock Text="{Binding Name}"
                               TextAlignment="Center"
                               FontWeight="Medium" />
                </DataTemplate>
            </ListBox.ItemTemplate>

            <ListBox.Styles>
                <Style Selector="ListBoxItem">
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
                </Style>
            </ListBox.Styles>

        </ListBox>
    </StackPanel>
</UserControl>

ViewModel:
A class MainViewModel, that contains an Items property with each item having an IsSelected property which it TwoWay bound to the ListBoxItem's IsSelected property.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using AvaloniaListBoxMultiselectToggle.Models;
using DynamicData;
using ReactiveUI;

namespace AvaloniaListBoxMultiselectToggle.ViewModels;

/*
 * The program initializes with all items being selected. Clicking Item1 deselects
 * Item1 and the All button, and this is the correct behavior. However, at this point
 * clicking the All button does not select either the All button or the Item1 button
 * as it's supposed to. Clicking the All button a second time does correctly select
 * the All button and the Item1 button.
 */

public class MainViewModel : ReactiveObject
{
    private OptionItem? _allItem;
    private int _isUpdating;
    private IList<OptionItem>? _otherItems;

    public MainViewModel()
    {
        Items = new ObservableCollection<OptionItem>();
        InitializeItems();
    }

    public ObservableCollection<OptionItem> Items { get; }


    public void InitializeItems()
    {
        // Add "All" item and other items
        _allItem = new OptionItem("All", true);
        _otherItems =
        [
            new OptionItem("Item1", true),
            new OptionItem("Item2", true),
            new OptionItem("Item3", true)
        ];

        Items.Add(_allItem);
        Items.AddRange(_otherItems);

        // Subscribe to "All" item changes
        _allItem.WhenAnyValue(x => x.IsSelected)
            .Subscribe(isSelected =>
            {
                // If you put a breakpoint here, it will break after you click
                // Item1 which in turns deselects the All button, triggering
                // this handler. Subsequently clicking the All button will not
                // trigger a change event from the ListBox, and this breakpoint
                // will not hit.
                if (Interlocked.Exchange(ref _isUpdating, 1) == 1)
                {
                    return;
                }

                try
                {
                    UpdateOtherItems(isSelected);
                }
                finally
                {
                    Interlocked.Exchange(ref _isUpdating, 0);
                }
            });

        // Subscribe to other item changes
        foreach (var otherItem in _otherItems)
        {
            otherItem.WhenAnyValue(x => x.IsSelected)
                .Subscribe(_ =>
                {
                    if (Interlocked.Exchange(ref _isUpdating, 1) == 1)
                    {
                        return;
                    }

                    try
                    {
                        UpdateAllItem();
                    }
                    finally
                    {
                        Interlocked.Exchange(ref _isUpdating, 0);
                    }
                });
        }
    }

    private void UpdateAllItem()
    {
        // Select "All" if all other items are selected; otherwise deselect it.
        if (_allItem != null && _otherItems != null)
        {
            _allItem.IsSelected = _otherItems.All(x => x.IsSelected);
        }
    }

    private void UpdateOtherItems(bool isSelected)
    {
        // If "All" is selected, select all other items; otherwise deselect them.
        if (_otherItems != null)
        {
            foreach (var item in _otherItems)
            {
                item.IsSelected = isSelected;
            }
        }
    }
}

OptionItem:
Represents each item in the ListBox

using ReactiveUI;

namespace AvaloniaListBoxMultiselectToggle.Models;

public class OptionItem : ReactiveObject
{
    private bool _isSelected;

    public OptionItem(string name, bool isSelected)
    {
        Name = name;
        _isSelected = isSelected;
    }

    public string Name { get; }

    public bool IsSelected
    {
        get => _isSelected;
        set => this.RaiseAndSetIfChanged(ref _isSelected, value);
    }
}

Expected behavior

When the user interacts with the ListBox to change the selection state of one item and needs that change to select or deselect another item in the ListBox, changes to the view model should always reflect the correctly updated items in the ListBox.

Avalonia version

11.2.3, 11.1.0, 11.0.10

OS

Windows 11 24H2

Additional context

Full solution to repro:

AvaloniaListBoxMultiselectToggle.zip

@skataben skataben added the bug label Jan 24, 2025
@timunie
Copy link
Contributor

timunie commented Jan 25, 2025

You have a custom control here. Please test plain ListBox instead.

@skataben
Copy link
Author

You have a custom control here. Please test plain ListBox instead.

Per your request, I have refactored it so that it is a plain ListBox control.

@timunie
Copy link
Contributor

timunie commented Jan 29, 2025

I can see the issue but don't have an idea where exactly the root cause is. What I prefer if I have a "complex" selection is to implement ISelectionModel

@skataben
Copy link
Author

I can see the issue but don't have an idea where exactly the root cause is. What I prefer if I have a "complex" selection is to implement ISelectionModel

Could you clarify please?

@timunie
Copy link
Contributor

timunie commented Jan 30, 2025

Just add the interface to your viewmodel or any other class and bind this to ListBox.Selection

@skataben
Copy link
Author

skataben commented Feb 1, 2025

Just add the interface to your viewmodel or any other class and bind this to ListBox.Selection

Thanks! I will try that. Are there any examples of this binding anywhere? I can't find a single one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants