Search code examples
c#xamldata-bindinguwptreeview

Children missing when changing ItemsSource of TreeView


I have multiple ObservableCollections of Items with children. I am trying to bind a TreeView to a collection, where the user will select the source during runtime. The user is able to drag and drop on the list to reorganise/edit the active source collection.

What I have tried works perfectly as long as you never switch to the previously selected source. Switching back to the previous source will result in missing children, with only the root items being displayed. This behaviour becomes even more inconsistent if children had been left expanded before returning, some children remaining, and others not.

xaml:

<Grid RowSpacing="8" Margin="8">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Spacing="8">
        <Button x:Name="ShowC1" Click="ShowC1_Click" Content="Container 1"/>
        <Button x:Name="ShowC2" Click="ShowC2_Click" Content="Container 2"/>
        <Button x:Name="ShowC3" Click="ShowC3_Click" Content="Container 3"/>
    </StackPanel>
    <muxc:TreeView x:Name="ItemTree" ItemsSource="{x:Bind Source}" Grid.Row="1">
        <muxc:TreeView.ItemTemplate>
            <DataTemplate x:DataType="local:Item">
                <muxc:TreeViewItem ItemsSource="{x:Bind Children}" Content="{x:Bind Name}"/>
            </DataTemplate>
        </muxc:TreeView.ItemTemplate>
    </muxc:TreeView>
</Grid>

xaml.cs

public sealed partial class MainPage : Page
{
    private Container _con1, _con2, _con3;
    ExtendedObservableCollection<Item> Source = new ExtendedObservableCollection<Item>();

    public MainPage()
    {
        this.InitializeComponent();
        _con1 = new Container("Container 1", 2);
        _con2 = new Container("Container 2", 3);
        _con3 = new Container("Container 3", 4);
    }

    private void ShowC1_Click(object sender, RoutedEventArgs e)
    {
        Source.ReplaceAll(_con1.Items);
    }

    private void ShowC2_Click(object sender, RoutedEventArgs e)
    {
        Source.ReplaceAll(_con2.Items);
    }

    private void ShowC3_Click(object sender, RoutedEventArgs e)
    {
        Source.ReplaceAll(_con3.Items);
    }
}

public class Container
{
    public string Title { get; set; } = "Untitled";
    public ObservableCollection<Item> Items { get; private set; } = new ObservableCollection<Item>();

    public Container(string title, int numItems)
    {
        Title = title;

        for (int i = 0; i < numItems; i++)
        {
            Items.Add(new Item("Item " + i, i + 1));
        }
    }
}

public class Item
{
    public string Name { get; set; } = "Item";
    public ObservableCollection<Item> Children { get; set; } = new ObservableCollection<Item>();

    public Item(string name, int numChildren)
    {
        Name = name;

        for (int i = 0; i < numChildren; i++)
        {
            Children.Add(new Item("Child " + i, 0));
        }
    }
}

ExtendedObservableCollection.cs

public class ExtendedObservableCollection<T> : ObservableCollection<T>
{
    public void ReplaceAll(ICollection<T> items)
    {
        this.Items.Clear();
        foreach (T item in items)
        {
            this.Items.Add(item);
        }
        this.OnCollectionChanged(
            new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)
        );
    }
}

Gif of issue: Code result

Update: Temporary workaround until I can find something nicer that works within what I need

Firstly, I noticed the collection wasn't even being updated from drag n' drop actions, but referencing the latest WinUI fixed that. I also changed the bindings to use x:Bind.

Second, I was able to get the children working in a rather dirty way by clearing the TreeView source, updating the bindings, then setting to a dummy collection (with at least 1 member, 0 results in failing to load only the last children for some reason), updating bindings, then finally setting it to the real source, and updating bindings one last time. Async delays were needed between each binding update to refresh the TreeView. The downside to this is that it introduces a frame of flicker for when the source is blank.


Solution

  • A fix for this issue was shipped with Microsoft.UI.Xaml v2.4.0. See this commit for more info.