Skip to content

ItemsRepeater with UserControls in datatemplate exhibits odd behaviour #10525

@torleifat

Description

@torleifat

Describe the bug

For a bit of context we have a UserControl, that we call a Carousel, which is basically a ItemsRepeater in a ScrollView.
Every time the user saves a some data that data is stored in an observable vector bound to the ItemsRepeater. The ItemsRepeater's DataTemplate has a custom UserControl in it, and we bind the item to the UserControl. Another crucial piece of this is that we use the SizeChanged event on the ItemsRepeater to scroll the viewport of the ScrollView to the latest saved item.

What we are seeing is that if the ItemsRepeater is loaded with a large list items and it auto-scrolls to the latest item the rendering of some of the UserControls in its datatemplate will not render properly. They will seemingly reuse how previous UserControls look, most of the time some of the first UserControls in the ItemsRepeater.

I've made a pared down version of the UserControls to demonstrate the behavior:

Here I have a list of items that we've bound to the ItemsRepeater.

        m_Items = winrt::single_threaded_observable_vector<hstring>();
        m_Items.Append(L"🐖");
        m_Items.Append(L"🐖");
        m_Items.Append(L"🐖");
        m_Items.Append(L"🐖");
        m_Items.Append(L"🥕");
        m_Items.Append(L"🥕");
        m_Items.Append(L"🥕");
        m_Items.Append(L"🥕");
        m_Items.Append(L"🥕");
        m_Items.Append(L"🥕");

The ItemsRepeater is defined as following:

<ScrollViewer x:Name="Scroller" Height="64"  Width="300" Background="Green" HorizontalScrollMode="Enabled" HorizontalScrollBarVisibility="Visible">
    <ItemsRepeater HorizontalCacheLength="100" SizeChanged="Repeater_SizeChanged" x:Name="Repeater" Layout="{StaticResource HorizontalStackLayout}" 
                   ItemsSource="{x:Bind Items}" ItemTemplate="{StaticResource ItemsRepeaterTemplateUserControlTemplate}"/>
</ScrollViewer>

and its data template is defined with:

<DataTemplate x:Key="ItemsRepeaterTemplateUserControlTemplate" x:DataType="x:String">
    <local:SimpleUserControl Data="{x:Bind}"></local:SimpleUserControl>
</DataTemplate>

The event code for Repeater_SizeChanged is as follows:

void MainPage::Repeater_SizeChanged(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::SizeChangedEventArgs const&)
{
    if (Scroller().ScrollableWidth() > 0)
    {
       Scroller().ChangeView(Scroller().ScrollableWidth(), nullptr, nullptr);
    }
}

Lastly the xaml code for the SimpleUserControl is as follows:

    <Grid>
        <Border Background="Red" BorderBrush="Black" BorderThickness="1" Width="64" Height="64">
            <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="38" Text="{x:Bind Data}"/>
        </Border>
    </Grid>

One would expect that you would get a list of 4 pigs followed by 6 carrots. However that is not exactly what you get:

Image

Here we see that the three last items look like pigs, but they should look like carrots. I've added code in the UserControls that set the tooltip on the user control to be whatever the Data is bound to:

void SimpleUserControl::Data(const winrt::hstring& Value)
{
	m_Data = Value;
	winrt::Microsoft::UI::Xaml::Controls::ToolTipService::SetToolTip(*this, winrt::box_value(Value));
}

Note if I had bound the tooltip to the value of Data in Xaml it would have had the same issue as with the TextBlock

If I replace the ItemsRepeater's DataTemplate with one that does not use a UserControl it will work as expected:

<DataTemplate x:Key="ItemsRepeaterTemplate" x:DataType="x:String"> <!-- Will work as expected. -->
    <Grid>
        <Border Background="Red" BorderBrush="Black" BorderThickness="1" Width="64" Height="64">
            <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="38" Text="{x:Bind}"/>
        </Border>
    </Grid>
</DataTemplate>

Image

Also if I remove the code that scrolls to the end of the ScrollView things will work as one expects. So it seems to me there is some sort of issue with the combination of using UserControls in the DataTemplate and at the same time scrolling to the end of the ScrollView.
Also I have set HorizontalCacheLength to 100 in this example. If I left it with whatever the default value is it would not work either.

So I assume this has to do with some sort of reuse in the ItemsRepeater, but I cannot quite understand why using a UserControl, and Scrolling to the end should change the behavior.

Steps to reproduce the bug

  1. Create a new project with a new Page
  2. Create a UserControl that that has as String property that you can bind into a TextBlock in the UserControl's xaml.
  3. Add an observable vector of hstrings to the page, and in the xaml define an ItemsRepeater inside a ScrollView. Define a DataTemplate that uses the UserControl, bind the vector to the ItemsRepeater, set HorizontalCacheLength to 100.
  4. Add a callback for the SizeChanged event in ItemsRepeater and in the callback make the ScrollView to always scroll to the end.
  5. Make observable vector has a lot of items that the ScrollView.

For more details see the code above, or here's a link to the test implementation I made: https://github.com/torleifat/UserControlInItemsRepeaterTest

Expected behavior

For it not to reuse UserControls.

Screenshots

No response

NuGet package version

WinUI 3 - Windows App SDK 1.7.1: 1.7.250401001

Windows version

Windows 11 (24H2): Build 26100

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions