microsoft / microsoft-ui-xaml

Windows UI Library: the latest Windows 10 native controls and Fluent styles for your applications
MIT License
6.35k stars 678 forks source link

Question: How can I safely Clear() the contents of an ObservableCollection that's serving as a TreeView's ItemsSource? #4075

Closed AJLeuer closed 3 years ago

AJLeuer commented 3 years ago

I'm writing WinUI 3 desktop application. The main window consists of a TreeView with the names of car makes as the parent items, and car models as the children.

My problem: Here's the sequence of events that leads to the mystery exception:

  1. The user clicks the "Delete" button.
  2. DeleteCarMake() is invoked via callback.
  3. DeleteCarMake() deletes the selected CarMake and completely resets the contents of the Cars ObservableCollection.
  4. The next time the user clicks on the TreeView an exception is thrown.

The problem seems to be connected to the complete removal of the contents of Cars and subsequent copying of data back into Cars (note that Cars serves as the ItemsSource for the TreeView). If I simply remove the carMakeToDelete from Cars, the exception isn't thrown next time the user clicks the TreeView. However, for reasons which my simplified code can't express, I need to be able to completely Clear() Cars' contents at every delete, not just remove a single item.

What I'd like to know: How can I safely Clear() the contents of my Cars ObservableCollection without causing an exception to be thrown the next time a user clicks on the TreeView?

One possible clue: If I don't define an event handler for the TreeView's SelectionChanged event in my MainWindow.xaml then the exception never occurs. I'm not quite sure what this implies, but hopefully someone else can put together the pieces of the puzzle.

Here's the relevant code (there's more than just this but I think these classes the ones that are germaine to the actual problem) :

MainWindow.xaml:

<Window
    x:Class="Cars.View.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:model="using:Cars.Model"
    xmlns:view="using:Cars.View"
    xmlns:utility="using:Cars.View.Utility">

    <Grid>
        <Grid.Resources>

            <DataTemplate x:Key="CarMakeTemplate" x:DataType="model:CarMake">
                <TreeViewItem ItemsSource="{x:Bind Path=CarModels, Mode=OneWay}">
                    <StackPanel Orientation="Horizontal">
                        <view:CarMakeView CarMake="{x:Bind Mode=OneWay}"/>
                        <Button Content="Delete" Click="DeleteCarMake"/>
                    </StackPanel>
                </TreeViewItem>
            </DataTemplate>

            <DataTemplate x:Key="CarModelTemplate" x:DataType="model:CarModel">
                <TreeViewItem>
                    <view:CarModelView CarModel="{x:Bind Mode=OneWay}"/>
                </TreeViewItem>
            </DataTemplate>

            <utility:CarItemSelector
                x:Key="CarItemSelector"
                CarMakeTemplate="{StaticResource CarMakeTemplate}"
                CarModelTemplate="{StaticResource CarModelTemplate}" />
        </Grid.Resources>

        <TreeView ItemsSource="{x:Bind Cars, Mode=OneWay}"  
                  ItemTemplateSelector="{StaticResource CarItemSelector}"
                  SelectionChanged="HandleSelectedCarMakeChanged">
        </TreeView>
    </Grid>

</Window>

MainWindow.cs:


public sealed partial class MainWindow : Window, INotifyPropertyChanged
    {
        public ObservableCollection<CarMake> Cars { get; set; } =
            new ()
            {
                new CarMake { Name = "Chevrolet", CarModels = { new CarModel { Name = "Camaro" }, new CarModel { Name = "Blazer" }, new CarModel { Name = "Beretta" } } },
                new CarMake { Name = "Land Rover", CarModels = { new CarModel { Name = "Discovery" }, new CarModel { Name = "LR3" }, new CarModel { Name = "Range Rover" } } },
                new CarMake { Name = "Quadra", CarModels = { new CarModel { Name = "Turbo-R 740" }, new CarModel { Name = "Type-66 Avenger" }} },
                new CarMake { Name = "Powell Motors", CarModels = { new CarModel { Name = "The Homer" }}}
            };

        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void DeleteCarMake(object sender, RoutedEventArgs eventInfo)
        {
            var carMakeToDelete = ((Button) eventInfo.OriginalSource).DataContext as CarMake;
            var updatedCarMakes = new List<CarMake>(Cars);
            updatedCarMakes.Remove(carMakeToDelete!);

            Cars.Clear();
            foreach (CarMake carMake in updatedCarMakes)
            {
                Cars.Add(carMake);
            }
            OnPropertyChanged(nameof(Cars));
        }

        private void HandleSelectedCarMakeChanged(TreeView sender, TreeViewSelectionChangedEventArgs info)
        {
            //do stuff
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        [NotifyPropertyChangedInvocator]
        private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
StephenLPeters commented 3 years ago

@AJLeuer I suspect this is the issue resolved by this https://github.com/microsoft/microsoft-ui-xaml/pull/3136 PR. If that is the case the fix for this will be in the next Winui3 Preview. You could confirm that your scenario will be fixed by trying out your treeview usage with winui2 if you'd like.

AJLeuer commented 3 years ago

Thanks Stephen. I guess I'll just have to wait for the next update then.