Offset Issue with items in ItemsRepeater inside a ScrollViewer when modifying Visibility
Describe the bug
So this is a bit of a convoluted one, but the gist of it is the following:
If you have an ItemsRepater contained within a ScrollViewer, with enough items in the repeater that the ScrollViewer can be scrolled. If you then modify the Visibility of the items, you can end up with items being offset outside the visible area of the ItemsRepeater/ScrollViewer
As an example
This is how things should work:
Clicking the respective buttons modifies the Visibility of the items in the ItemsRepeater.
However, if you have scrolled a little and try to do the same, then this will happen:
Now the orangutang appears to be gone, but it is only offset:
The blue square under ItemsRepeater is the actual Item. So it is there, and seemingly Visible, but offset so far that it cannot be seen.
It seems this will only happen when collapsing the later items in the collection. Also if there are so many items being made visible that the ScrollViewer can scroll then this will happen:
Here the first item is offset at first, but once we scroll it snaps back into the correct position.
Steps to reproduce the bug
- Created an empty project, and added a page that I navigate to from the MainWindow.
- The rest is pretty straight forward. The important bits is to have an ItemsRepeater with enough items that the ScrollViewer Containing it will be activated. And then a way to modify visibility.
Here's the code I had for page I used to make the images with:
MainPage.idl
namespace ScrollViewerAndItemsRepeaterIssue
{
[default_interface]
runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
{
MainPage();
Windows.Foundation.Collections.IObservableVector<String> Items{ get; };
}
}
MainPage.xaml
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="ScrollViewerAndItemsRepeaterIssue.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ScrollViewerAndItemsRepeaterIssue"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" Background="Aquamarine">
<Page.Resources>
<ResourceDictionary>
<StackLayout x:Name="HorizontalStackLayout" Orientation="Horizontal" Spacing="0"/>
<NonVirtualizingLayout x:Name="nvl" ></NonVirtualizingLayout>
<DataTemplate x:Key="ItemTemplate" x:DataType="x:String">
<Border Background="Red" BorderBrush="Black" BorderThickness="1" Width="64" Height="64">
<TextBlock FontSize="38" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{x:Bind}"></TextBlock>
</Border>
</DataTemplate>
</ResourceDictionary>
</Page.Resources>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<ScrollViewer x:Name="Scroller" Height="64" Width="300" Background="Green" HorizontalScrollMode="Enabled" HorizontalScrollBarVisibility="Visible">
<ItemsRepeater x:Name="Repeater" Layout="{StaticResource HorizontalStackLayout}" ItemsSource="{x:Bind Items}" ItemTemplate="{StaticResource ItemTemplate}"/>
</ScrollViewer>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Tag="orangutang" Click="Button_Click">Show only 🍌</Button>
<Button Tag="banana" Click="Button_Click">Show only 🦧</Button>
</StackPanel>
</StackPanel>
</Page>
MainPage.xaml.h
#pragma once
#include "MainPage.g.h"
namespace winrt::ScrollViewerAndItemsRepeaterIssue::implementation
{
struct MainPage : MainPageT<MainPage>
{
MainPage();
void FilterOutBanana();
void FilterOutOrangutang();
void Button_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
winrt::Windows::Foundation::Collections::IObservableVector<hstring> Items();
winrt::Windows::Foundation::Collections::IObservableVector<hstring> m_Items;
};
}
namespace winrt::ScrollViewerAndItemsRepeaterIssue::factory_implementation
{
struct MainPage : MainPageT<MainPage, implementation::MainPage>{};
}
MainPage.xaml.cpp
#include "pch.h"
#include "MainPage.xaml.h"
#if __has_include("MainPage.g.cpp")
#include "MainPage.g.cpp"
#endif
using namespace winrt;
using namespace Microsoft::UI::Xaml;
using namespace Windows::Foundation;
namespace winrt::ScrollViewerAndItemsRepeaterIssue::implementation
{
MainPage::MainPage()
{
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"🍌");
}
winrt::Windows::Foundation::Collections::IObservableVector<hstring> MainPage::Items()
{
return m_Items;
}
void MainPage::Button_Click(IInspectable const& sender, RoutedEventArgs const& e)
{
auto toFilterOut = winrt::unbox_value<hstring>(sender.as<Controls::Button>().Tag());
if (toFilterOut == L"orangutang")
{
FilterOutOrangutang();
}
else if (toFilterOut == L"banana")
{
FilterOutBanana();
}
}
void MainPage::FilterOutBanana()
{
for (uint32_t i = 0; i < m_Items.Size(); i++)
{
auto element = Repeater().TryGetElement(i);
if (i == 0)
{
element.Visibility(Visibility::Visible);
}
else
{
element.Visibility(Visibility::Collapsed);
}
}
}
void MainPage::FilterOutOrangutang()
{
for (uint32_t i = 0; i < m_Items.Size(); i++)
{
auto element = Repeater().TryGetElement(i);
if (i == 0)
{
element.Visibility(Visibility::Collapsed);
}
else
{
element.Visibility(Visibility::Visible);
}
}
}
}
Expected behavior
The Item not to be offset out of view.
Screenshots
No response
NuGet package version
WinUI 3 - Windows App SDK 1.5.4: 1.5.240607001
Windows version
Windows 11 (22H2): Build 22621
Additional context
No response
Hi I'm an AI powered bot that finds similar issues based off the issue title.
Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one. Thank you!
Open similar issues:
- ItemsRepeater inside a ScrollViewer with odd behaviour when it changes size (#9584), similarity score: 0.84
- Incorrect position for the first item in ItemsRepeater bound to an IObservableVector when inserting items from the front (#9743), similarity score: 0.83
Note: You can give me feedback by thumbs upping or thumbs downing this comment.
Thanks for your report torleifat. Each ItemsRepeater Layout subclass supports Collapsed elements more or less. In general, collapsing ItemsRepeater children is really not recommended, in particular not for virtualizing layouts. We do not explicitly prevent it at the ItemsRepeater level because it's really a Layout-specific concern.
For example, consider the UniformGridLayout. It uses the first child to determine the children size in general. If it's collapsed, no element will show up (you can give it a try to see that).
As for the StackLayout layout that you are using, it uses the realized items sizes to predict the overall collection extent. If some of those are collapsed, that prediction is poor.
Anyways, because of the virtualization of the items, items get recycled as the user scrolls the content. If an element was collapsed, as it gets recycled and used for another index, it will remain collapsed. This is wrong, and it's an application code problem. The app needs to keep track of which elements are supposed to be collapsed and which are not.
Here's an example in C#:
public sealed partial class TestPage : Page
{
private List<int> _collapsedIndices = new List<int>();
private ObservableCollection<string> _colStrings = null;
public TestPage()
{
this.InitializeComponent();
if (itemsRepeater != null)
{
itemsRepeater.ElementPrepared += ItemsRepeater_ElementPrepared;
_colStrings = new ObservableCollection<string>();
for (int itemIndex = 0; itemIndex < 200; itemIndex++)
_colStrings.Add("Item" + itemIndex);
itemsRepeater.ItemsSource = _colStrings;
}
}
private void BtnChangeEvenItemsVisibility_Click(object sender, RoutedEventArgs e)
{
if (itemsRepeater != null && _colStrings != null)
{
Visibility newVisibility = Visibility.Collapsed;
if (btnChangeEvenItemsVisibility.Content as string == "Collapse even items")
{
btnChangeEvenItemsVisibility.Content = "Show even items";
}
else
{
btnChangeEvenItemsVisibility.Content = "Collapse even items";
newVisibility = Visibility.Visible;
}
for (int i = 0; i < (_colStrings.Count + 1) / 2; i++)
{
UIElement element = itemsRepeater.TryGetElement(2 * i);
if (element != null)
{
element.Visibility = newVisibility;
if (newVisibility == Visibility.Visible)
{
if (_collapsedIndices.Contains(2 * i))
{
_collapsedIndices.Remove(2 * i);
}
}
else
{
if (!_collapsedIndices.Contains(2 * i))
{
_collapsedIndices.Add(2 * i);
}
}
}
}
}
}
private void BtnChangeOddItemsVisibility_Click(object sender, RoutedEventArgs e)
{
if (itemsRepeater != null && _colStrings != null)
{
Visibility newVisibility = Visibility.Collapsed;
if (btnChangeOddItemsVisibility.Content as string == "Collapse odd items")
{
btnChangeOddItemsVisibility.Content = "Show odd items";
}
else
{
btnChangeOddItemsVisibility.Content = "Collapse odd items";
newVisibility = Visibility.Visible;
}
for (int i = 0; i < _colStrings.Count / 2; i++)
{
UIElement element = itemsRepeater.TryGetElement(2 * i + 1);
if (element != null)
{
element.Visibility = newVisibility;
if (newVisibility == Visibility.Visible)
{
if (_collapsedIndices.Contains(2 * i + 1))
{
_collapsedIndices.Remove(2 * i + 1);
}
}
else
{
if (!_collapsedIndices.Contains(2 * i + 1))
{
_collapsedIndices.Add(2 * i + 1);
}
}
}
}
}
}
private void ItemsRepeater_ElementPrepared(ItemsRepeater sender, ItemsRepeaterElementPreparedEventArgs args)
{
if (_collapsedIndices.Contains(args.Index))
{
if (args.Element.Visibility == Visibility.Visible)
args.Element.Visibility = Visibility.Collapsed;
}
else if (args.Element.Visibility == Visibility.Collapsed)
args.Element.Visibility = Visibility.Visible;
}
}
The key points are:
- the app keeps track of the collapsed indices in _collapsedIndices.
- when an old element is re-used, the ItemsRepeater_ElementPrepared event handler makes sure that it has the correct Visibility.
But there is a preferred way of handling items that should have no UI representation: filter the ItemsRepeater.ItemsSource collection by removing from it the indexes that you wanted collapsed. Make it an observable collection of course, so that the ItemsRepeater becomes aware of the source changes. That's the proper way of doing it. Hope this helps. Thanks.
Thanks for the insight, RBrid. Appreciate it!