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. Here's the relevant code:
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));
}
}
The problem: Here's the sequence of events that leads to the mystery exception:
- The user clicks the "Delete" button.
DeleteCarMake()is invoked via callback.DeleteCarMake()deletes the selectedCarMakeand completely resets the contents of theCarsObservableCollection.- The next time the user clicks on the
TreeViewan 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.
The full call stack is here but as best I can tell the relevant part seems to be in the middle:
Microsoft.UI.Xaml.Controls.dll!ViewModel::UpdateNodeSelection(struct winrt::Microsoft::UI::Xaml::Controls::TreeViewNode const &,enum TreeViewNode::TreeNodeSelectionState const &) Unknown
Microsoft.UI.Xaml.Controls.dll!ViewModel::UpdateSelection(struct winrt::Microsoft::UI::Xaml::Controls::TreeViewNode const &,enum TreeViewNode::TreeNodeSelectionState const &) Unknown
Microsoft.UI.Xaml.Controls.dll!SelectedTreeNodeVector::UpdateSelection(struct winrt::Microsoft::UI::Xaml::Controls::TreeViewNode const &,enum TreeViewNode::TreeNodeSelectionState) Unknown
Microsoft.UI.Xaml.Controls.dll!SelectedTreeNodeVector::RemoveAt(unsigned int) Unknown
Microsoft.UI.Xaml.Controls.dll!SelectedTreeNodeVector::Clear(void) Unknown
Microsoft.UI.Xaml.Controls.dll!winrt::impl::produce<class SelectedTreeNodeVector,struct winrt::Windows::Foundation::Collections::IVector<struct winrt::Microsoft::UI::Xaml::Controls::TreeViewNode> >::Clear(void) Unknown
Microsoft.UI.Xaml.Controls.dll!winrt::impl::consume_Windows_Foundation_Collections_IVector<struct winrt::Windows::Foundation::Collections::IVector<struct winrt::Microsoft::UI::Xaml::DependencyObject>,struct winrt::Microsoft::UI::Xaml::DependencyObject>::Clear(void) Unknown
Microsoft.UI.Xaml.Controls.dll!ViewModel::SelectNode(struct winrt::Microsoft::UI::Xaml::Controls::TreeViewNode const &,bool) Unknown
Microsoft.UI.Xaml.Controls.dll!TreeView::UpdateSelection(struct winrt::Microsoft::UI::Xaml::Controls::TreeViewNode const &,bool) Unknown
Microsoft.UI.Xaml.Controls.dll!TreeViewItem::UpdateSelection(bool) Unknown
Microsoft.UI.Xaml.Controls.dll!TreeViewItem::OnIsSelectedChanged(struct winrt::Microsoft::UI::Xaml::DependencyObject const &,struct winrt::Microsoft::UI::Xaml::DependencyProperty const &) Unknown
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.
Carsproperty rather then resetting and repopulating the existing list, of courseCarswould need to notify of property change. I have seen UIs (third party controls) struggle or crash in the past due to the way people have updated a bound ObservableCollection, sometimes it is more efficient to create a new one. Of course these comments do not necessarily solve your particular problem, hence why they're comments and not an answer :)