public void ValidateStackLayoutDoesNotRetainIncorrectMinorWidth() { RunOnUIThread.Execute(() => { var repeater = new ItemsRepeater() { ItemsSource = Enumerable.Range(0, 1) }; Content = new ScrollViewer() { Content = repeater, Width = 400, }; Content.UpdateLayout(); // Measure with large width. repeater.Measure(new Size(600, 100)); Verify.AreEqual(600, repeater.DesiredSize.Width); // Measure with smaller width again before arrange. // StackLayout has to pick up the smaller width for its extent. repeater.Measure(new Size(300, 100)); Verify.AreEqual(300, repeater.DesiredSize.Width); Content.UpdateLayout(); Verify.AreEqual(400, repeater.ActualWidth); }); }
private ItemsRepeaterScrollHost CreateAndInitializeRepeater( object itemsSource, VirtualizingLayout layout, object elementFactory, ref ItemsRepeater repeater, ref ScrollViewer scrollViewer) { repeater = new ItemsRepeater() { ItemsSource = itemsSource, Layout = layout, ItemTemplate = elementFactory, HorizontalCacheLength = 0, VerticalCacheLength = 0, }; scrollViewer = new ScrollViewer() { Content = repeater }; return(new ItemsRepeaterScrollHost() { Width = 400, Height = 400, ScrollViewer = scrollViewer }); }
public void ValidateStackLayoutDisabledVirtualizationWithItemsRepeater() { RunOnUIThread.Execute(() => { var repeater = new ItemsRepeater(); var stackLayout = new StackLayout(); stackLayout.DisableVirtualization = true; repeater.Layout = stackLayout; repeater.ItemsSource = Enumerable.Range(0, 10); repeater.ItemTemplate = (DataTemplate)XamlReader.Parse( @"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'> <Button Content='{Binding}' Height='100' /> </DataTemplate>"); var scrollViewer = new ScrollViewer() { Content = repeater }; scrollViewer.Height = 100; Content = scrollViewer; Content.UpdateLayout(); for (int i = 0; i < repeater.ItemsSourceView.Count; i++) { var child = repeater.TryGetElement(i) as Button; Verify.IsNotNull(child); } }); }
public void ValidateNonVirtualLayoutWithItemsRepeater() { RunOnUIThread.Execute(() => { var repeater = new ItemsRepeater(); repeater.Layout = new NonVirtualStackLayout(); repeater.ItemsSource = Enumerable.Range(0, 10); repeater.ItemTemplate = (DataTemplate)XamlReader.Parse( @"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'> <Button Content='{Binding}' Height='100' /> </DataTemplate>"); Content = repeater; Content.UpdateLayout(); double expectedYOffset = 0; for (int i = 0; i < repeater.ItemsSourceView.Count; i++) { var child = repeater.TryGetElement(i) as Button; Verify.IsNotNull(child); var layoutBounds = LayoutInformation.GetLayoutSlot(child); Verify.AreEqual(expectedYOffset, layoutBounds.Y); Verify.AreEqual(i, child.Content); expectedYOffset += 100; } }); }
public void VerifyClearingItemsSourceClearsElements() { var data = new ObservableCollection <string>(Enumerable.Range(0, 4).Select(i => "Item #" + i)); var mapping = (List <ContentControl>)null; RunOnUIThread.Execute(() => { mapping = Enumerable.Range(0, data.Count).Select(i => new ContentControl { Width = 40, Height = 40 }).ToList(); var dataSource = MockItemsSource.CreateDataSource(data, supportsUniqueIds: false); var elementFactory = MockElementFactory.CreateElementFactory(mapping); ItemsRepeater repeater = new ItemsRepeater(); repeater.ItemsSource = dataSource; repeater.ItemTemplate = elementFactory; // This was an issue only for NonVirtualizing layouts repeater.Layout = new MyCustomNonVirtualizingStackLayout(); Content = repeater; Content.UpdateLayout(); repeater.ItemsSource = null; }); foreach (var item in mapping) { Verify.IsNull(item.Parent); } }
public void ValidateRepeaterDefaults() { RunOnUIThread.Execute(() => { var repeater = new ItemsRepeater() { ItemsSource = Enumerable.Range(0, 10).Select(i => string.Format("Item #{0}", i)), }; Content = new ItemsRepeaterScrollHost() { Width = 400, Height = 800, ScrollViewer = new ScrollViewer { Content = repeater } }; Content.UpdateLayout(); for (int i = 0; i < 10; i++) { var element = repeater.TryGetElement(i); Verify.IsNotNull(element); Verify.AreEqual(string.Format("Item #{0}", i), ((TextBlock)element).Text); Verify.AreEqual(i, repeater.GetElementIndex(element)); } Verify.IsNull(repeater.TryGetElement(20)); }); }
public void ValidateGetSetItemsSource() { RunOnUIThread.Execute(() => { ItemsRepeater repeater = new ItemsRepeater(); var dataSource = new ItemsSourceView(Enumerable.Range(0, 10).Select(i => string.Format("Item #{0}", i))); repeater.SetValue(ItemsRepeater.ItemsSourceProperty, dataSource); Verify.AreSame(dataSource, repeater.GetValue(ItemsRepeater.ItemsSourceProperty) as ItemsSourceView); Verify.AreSame(dataSource, repeater.ItemsSourceView); }); }
public void ValidateMappingAndAutoRecycling() { ItemsRepeater repeater = null; ScrollViewer scrollViewer = null; RunOnUIThread.Execute(() => { var layout = new MockVirtualizingLayout() { MeasureLayoutFunc = (availableSize, context) => { var element0 = context.GetOrCreateElementAt(index: 0); // lookup - repeater will give back the same element and note that this element will not // be pinned - i.e it will be auto recycled after a measure pass where GetElementAt(0) is not called. var element0lookup = context.GetOrCreateElementAt(index: 0, options: ElementRealizationOptions.None); var element1 = context.GetOrCreateElementAt(index: 1, options: ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); // forcing a new element for index 1 that will be pinned (not auto recycled). This will be // a completely new element. Repeater does not do the mapping/lookup when forceCreate is true. var element1Clone = context.GetOrCreateElementAt(index: 1, options: ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); Verify.AreSame(element0, element0lookup); Verify.AreNotSame(element1, element1Clone); element0.Measure(availableSize); element1.Measure(availableSize); element1Clone.Measure(availableSize); return(new Size(100, 100)); }, }; Content = CreateAndInitializeRepeater( itemsSource: Enumerable.Range(0, 5), layout: layout, elementFactory: GetDataTemplate("<Button>Hello</Button>"), repeater: ref repeater, scrollViewer: ref scrollViewer); Content.UpdateLayout(); Verify.IsNotNull(repeater.TryGetElement(0)); Verify.IsNotNull(repeater.TryGetElement(1)); layout.MeasureLayoutFunc = null; repeater.InvalidateMeasure(); Content.UpdateLayout(); Verify.IsNull(repeater.TryGetElement(0)); // not pinned, should be auto recycled. Verify.IsNotNull(repeater.TryGetElement(1)); // pinned, should stay alive }); }
public void VerifyRepeaterDoesNotLeakItemContainers() { ObservableCollection <int> items = new ObservableCollection <int>(); for (int i = 0; i < 10; i++) { items.Add(i); } ItemsRepeater repeater = null; RunOnUIThread.Execute(() => { var template = (DataTemplate)XamlReader.Parse("<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' " + "xmlns:local='clr-namespace:MUXControlsTestApp.Samples;assembly=MUXControlsTestApp'>" + "<local:DisposableUserControl Number='{Binding}'/>" + "</DataTemplate>"); Verify.IsNotNull(template); Verify.AreEqual(0, MUXControlsTestApp.Samples.DisposableUserControl.OpenItems, "Verify we start with 0 DisposableUserControl"); repeater = new ItemsRepeater() { ItemsSource = items, ItemTemplate = template, VerticalAlignment = VerticalAlignment.Top, HorizontalAlignment = HorizontalAlignment.Left }; Content = repeater; }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Verify.IsGreaterThanOrEqual(MUXControlsTestApp.Samples.DisposableUserControl.OpenItems, 10, "Verify we created at least 10 DisposableUserControl"); // Clear out the repeater and make sure everything gets cleaned up. Content = null; repeater = null; }); IdleSynchronizer.Wait(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Verify.AreEqual(0, MUXControlsTestApp.Samples.DisposableUserControl.OpenItems, "Verify we cleaned up all the DisposableUserControl that were created"); }
public void VerifyUniformGridLayoutDoesntCrashWhenTryingToScrollToEnd() { ItemsRepeater repeater = null; ScrollViewer scrollViewer = null; RunOnUIThread.Execute(() => { repeater = new ItemsRepeater { ItemsSource = Enumerable.Range(0, 1000).Select(i => new Border { Background = new SolidColorBrush(Colors.Blue), Child = new TextBlock { Text = "#" + i } }).ToArray(), Layout = new UniformGridLayout { MinItemWidth = 100, MinItemHeight = 40, MinRowSpacing = 10, MinColumnSpacing = 10 } }; scrollViewer = new ScrollViewer { Content = repeater }; Content = scrollViewer; }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { scrollViewer.ChangeView(0, repeater.ActualHeight, null); }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { scrollViewer.ChangeView(0, 0, null); }); IdleSynchronizer.Wait(); // The test guards against an app crash, so this is enough to verify Verify.IsTrue(true); }
public void ValidateGetSetBackground() { RunOnUIThread.Execute(() => { ItemsRepeater repeater = new ItemsRepeater(); var redBrush = new SolidColorBrush(Colors.Red); repeater.SetValue(ItemsRepeater.BackgroundProperty, redBrush); Verify.AreSame(redBrush, repeater.GetValue(ItemsRepeater.BackgroundProperty) as Brush); Verify.AreSame(redBrush, repeater.Background); var blueBrush = new SolidColorBrush(Colors.Blue); repeater.Background = blueBrush; Verify.AreSame(blueBrush, repeater.Background); }); }
public void VerifyUIElementsInItemsSource() { ItemsRepeater repeater = null; RunOnUIThread.Execute(() => { var scrollhost = (ItemsRepeaterScrollHost)XamlReader.Parse( @"<controls:ItemsRepeaterScrollHost xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='clr-namespace:MUXControlsTestApp.Samples;assembly=MUXControlsTestApp' xmlns:controls='http://schemas.modernwpf.com/2019'> <ScrollViewer> <controls:ItemsRepeater x:Name='repeater'> <controls:ItemsRepeater.ItemsSource> <local:UICollection> <Button>0</Button> <Button>1</Button> <Button>2</Button> <Button>3</Button> <Button>4</Button> <Button>5</Button> <Button>6</Button> <Button>7</Button> <Button>8</Button> <Button>9</Button> </local:UICollection> </controls:ItemsRepeater.ItemsSource> </controls:ItemsRepeater> </ScrollViewer> </controls:ItemsRepeaterScrollHost>"); repeater = (ItemsRepeater)scrollhost.FindName("repeater"); Content = scrollhost; }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { for (int i = 0; i < 10; i++) { var element = repeater.TryGetElement(i) as Button; Verify.AreEqual(i.ToString(), element.Content); } }); }
public void ValidateElementToIndexMapping() { ItemsRepeater repeater = null; RunOnUIThread.Execute(() => { var elementFactory = new RecyclingElementFactory(); elementFactory.RecyclePool = new RecyclePool(); elementFactory.Templates["Item"] = (DataTemplate)XamlReader.Parse( @"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'> <TextBlock Text='{Binding}' Height='50' /> </DataTemplate>"); repeater = new ItemsRepeater() { ItemsSource = Enumerable.Range(0, 10).Select(i => string.Format("Item #{0}", i)), ItemTemplate = elementFactory, // Default is StackLayout, so do not have to explicitly set. // Layout = new StackLayout(), }; Content = new ItemsRepeaterScrollHost() { Width = 400, Height = 800, ScrollViewer = new ScrollViewer { Content = repeater } }; Content.UpdateLayout(); for (int i = 0; i < 10; i++) { var element = repeater.TryGetElement(i); Verify.IsNotNull(element); Verify.AreEqual(string.Format("Item #{0}", i), ((TextBlock)element).Text); Verify.AreEqual(i, repeater.GetElementIndex(element)); } Verify.IsNull(repeater.TryGetElement(20)); }); }
public void CanSetItemsSource() { // In bug 12042052, we crash when we set ItemsSource to null because we try to subscribe to // the DataSourceChanged event on a null instance. RunOnUIThread.Execute(() => { { var repeater = new ItemsRepeater(); repeater.ItemsSource = null; repeater.ItemsSource = Enumerable.Range(0, 5).Select(i => string.Format("Item #{0}", i)); } { var repeater = new ItemsRepeater(); repeater.ItemsSource = Enumerable.Range(0, 5).Select(i => string.Format("Item #{0}", i)); repeater.ItemsSource = Enumerable.Range(5, 5).Select(i => string.Format("Item #{0}", i)); repeater.ItemsSource = null; repeater.ItemsSource = Enumerable.Range(10, 5).Select(i => string.Format("Item #{0}", i)); repeater.ItemsSource = null; } }); }
private UIElement GetElementFromElementFactory(int index) { // The view generator is the provider of last resort. var data = m_owner.ItemsSourceView.GetAt(index); UIElement initElement() { var providedElementFactory = m_owner.ItemTemplateShim; if (providedElementFactory == null) { if (data is UIElement dataAsElement) { return(dataAsElement); } } IElementFactoryShim initElementFactory() { if (providedElementFactory == null) { var factory = XamlReader.Parse("<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'><TextBlock Text='{Binding}'/></DataTemplate>") as DataTemplate; m_owner.ItemTemplate = factory; return(m_owner.ItemTemplateShim); } return(providedElementFactory); } var elementFactory = initElementFactory(); ElementFactoryGetArgs initArgs() { if (m_ElementFactoryGetArgs == null) { m_ElementFactoryGetArgs = new ElementFactoryGetArgs(); } return(m_ElementFactoryGetArgs); } var args = initArgs(); try { args.Data = data; args.Parent = m_owner; args.Index = index; return(elementFactory.GetElement(args)); } finally { args.Data = null; args.Parent = null; } } var element = initElement(); var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) { virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); } else { // View obtained from ElementFactory already has a VirtualizationInfo attached to it // which means that the element has been recycled and not created from scratch. } // Clear flag virtInfo.MustClearDataContext = false; if (data != element) { if (element is FrameworkElement elementAsFE) { // Set data context only if no x:Bind was used. ie. No data template component on the root. // If the passed in data is a UIElement and is different from the element returned by // the template factory then we need to propagate the DataContext. // Otherwise just set the DataContext on the element as the data. object initElementDataContext() { if (data is FrameworkElement dataAsElement) { var dataDataContext = dataAsElement.DataContext; if (dataDataContext != null) { return(dataDataContext); } } return(data); } var elementDataContext = initElementDataContext(); elementAsFE.DataContext = elementDataContext; virtInfo.MustClearDataContext = true; } else { Debug.Assert(false, "Element returned by factory is not a FrameworkElement!"); } } virtInfo.MoveOwnershipToLayoutFromElementFactory( index, /* uniqueId: */ m_owner.ItemsSourceView.HasKeyIndexMapping ? m_owner.ItemsSourceView.KeyFromIndex(index) : string.Empty); // The view generator is the only provider that prepares the element. var repeater = m_owner; // Add the element to the children collection here before raising OnElementPrepared so // that handlers can walk up the tree in case they want to find their IndexPath in the // nested case. var children = repeater.Children; if (CachedVisualTreeHelpers.GetParent(element) != repeater) { children.Add(element); } repeater.AnimationManager.OnElementPrepared(element); repeater.OnElementPrepared(element, index); // Update realized indices m_firstRealizedElementIndexHeldByLayout = Math.Min(m_firstRealizedElementIndexHeldByLayout, index); m_lastRealizedElementIndexHeldByLayout = Math.Max(m_lastRealizedElementIndexHeldByLayout, index); return(element); }
public void VerifyStoreScenarioCache() { ItemsRepeater rootRepeater = null; RunOnUIThread.Execute(() => { var scrollhost = (ItemsRepeaterScrollHost)XamlReader.Parse( @" <controls:ItemsRepeaterScrollHost Width='400' Height='200' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:controls='http://schemas.modernwpf.com/2019'> <controls:ItemsRepeaterScrollHost.Resources> <DataTemplate x:Key='ItemTemplate' > <TextBlock Text='{Binding}' Height='100' Width='100'/> </DataTemplate> <DataTemplate x:Key='GroupTemplate'> <StackPanel> <TextBlock Text='{Binding}' /> <controls:ItemsRepeaterScrollHost> <ScrollViewer HorizontalScrollBarVisibility='Auto' VerticalScrollBarVisibility='Hidden'> <controls:ItemsRepeater ItemTemplate='{StaticResource ItemTemplate}' ItemsSource='{Binding}'> <controls:ItemsRepeater.Layout> <controls:StackLayout Orientation='Horizontal' /> </controls:ItemsRepeater.Layout> </controls:ItemsRepeater> </ScrollViewer> </controls:ItemsRepeaterScrollHost> </StackPanel> </DataTemplate> </controls:ItemsRepeaterScrollHost.Resources> <ScrollViewer x:Name='scrollviewer'> <controls:ItemsRepeater x:Name='rootRepeater' ItemTemplate='{StaticResource GroupTemplate}'/> </ScrollViewer> </controls:ItemsRepeaterScrollHost>"); rootRepeater = (ItemsRepeater)scrollhost.FindName("rootRepeater"); List <List <int> > items = new List <List <int> >(); for (int i = 0; i < 100; i++) { items.Add(Enumerable.Range(0, 4).ToList()); } rootRepeater.ItemsSource = items; Content = scrollhost; }); IdleSynchronizer.Wait(); // Verify that first items outside the visible range but in the realized range // for the inner of the nested repeaters are realized. RunOnUIThread.Execute(() => { // Group2 will be outside the visible range but within the realized range. var group2 = rootRepeater.TryGetElement(2) as StackPanel; Verify.IsNotNull(group2); var group2Repeater = ((ItemsRepeaterScrollHost)group2.Children[1]).ScrollViewer.Content as ItemsRepeater; Verify.IsNotNull(group2Repeater); Verify.IsNotNull(group2Repeater.TryGetElement(0)); }); }
public void VerifyCorrectionsInNonScrollableDirection() { ItemsRepeater rootRepeater = null; ScrollViewer scrollViewer = null; ItemsRepeaterScrollHost scrollhost = null; ManualResetEvent viewChanged = new ManualResetEvent(false); RunOnUIThread.Execute(() => { scrollhost = (ItemsRepeaterScrollHost)XamlReader.Parse( @"<controls:ItemsRepeaterScrollHost Width='400' Height='600' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:controls='http://schemas.modernwpf.com/2019'> <ScrollViewer Width='400' Height='400' x:Name='scrollviewer'> <controls:ItemsRepeater x:Name='repeater'> <DataTemplate> <StackPanel> <controls:ItemsRepeater ItemsSource='{Binding}'> <controls:ItemsRepeater.Layout> <controls:StackLayout Orientation='Horizontal' /> </controls:ItemsRepeater.Layout> </controls:ItemsRepeater> </StackPanel> </DataTemplate> </controls:ItemsRepeater> </ScrollViewer> </controls:ItemsRepeaterScrollHost>"); rootRepeater = (ItemsRepeater)scrollhost.FindName("repeater"); scrollViewer = (ScrollViewer)scrollhost.FindName("scrollviewer"); scrollViewer.ScrollChanged += (sender, args) => { if (args.HorizontalChange != 0 || args.VerticalChange != 0) { viewChanged.Set(); } }; List <List <int> > items = new List <List <int> >(); for (int i = 0; i < 100; i++) { items.Add(Enumerable.Range(0, 4).ToList()); } rootRepeater.ItemsSource = items; Content = scrollhost; }); // scroll down several times and validate no crash for (int i = 1; i < 5; i++) { IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { scrollViewer.ChangeView(null, i * 200, null); }); Verify.IsTrue(viewChanged.WaitOne(DefaultWaitTimeInMS)); viewChanged.Reset(); } }
private void NestedRepeaterWithDataTemplateScenario(bool disableAnimation) { /*if (!disableAnimation && PlatformConfiguration.IsOsVersionGreaterThanOrEqual(OSVersion.Redstone5)) * { * Log.Warning("This test is showing consistent issues with not scrolling enough on RS5 and 19H1 when animations are enabled, tracked by microsoft-ui-xaml#779"); * return; * }*/ // Example of how to include debug tracing in an ApiTests.ItemsRepeater test's output. // using (PrivateLoggingHelper privateLoggingHelper = new PrivateLoggingHelper("Repeater")) // { ItemsRepeater rootRepeater = null; ScrollViewer scrollViewer = null; ManualResetEvent viewChanged = new ManualResetEvent(false); RunOnUIThread.Execute(() => { var anchorProvider = (ItemsRepeaterScrollHost)XamlReader.Parse( @"<controls:ItemsRepeaterScrollHost Width='400' Height='600' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:controls='http://schemas.modernwpf.com/2019'> <controls:ItemsRepeaterScrollHost.Resources> <DataTemplate x:Key='ItemTemplate' > <TextBlock Text='{Binding}' /> </DataTemplate> <DataTemplate x:Key='GroupTemplate'> <StackPanel> <TextBlock Text='{Binding}' /> <controls:ItemsRepeater ItemTemplate='{StaticResource ItemTemplate}' ItemsSource='{Binding}' VerticalCacheLength='0'/> </StackPanel> </DataTemplate> </controls:ItemsRepeaterScrollHost.Resources> <ScrollViewer x:Name='scrollviewer'> <controls:ItemsRepeater x:Name='rootRepeater' ItemTemplate='{StaticResource GroupTemplate}' VerticalCacheLength='0' /> </ScrollViewer> </controls:ItemsRepeaterScrollHost>"); rootRepeater = (ItemsRepeater)anchorProvider.FindName("rootRepeater"); rootRepeater.SizeChanged += (sender, args) => { Log.Comment($"SizeChanged: Size=({rootRepeater.ActualWidth} x {rootRepeater.ActualHeight})"); }; scrollViewer = (ScrollViewer)anchorProvider.FindName("scrollviewer"); /*scrollViewer.ViewChanging += (sender, args) => * { * Log.Comment($"ViewChanging: Next VerticalOffset={args.NextView.VerticalOffset}, Final VerticalOffset={args.FinalView.VerticalOffset}"); * };*/ scrollViewer.ScrollChanged += (sender, args) => { if (args.HorizontalChange != 0 || args.VerticalChange != 0) { Log.Comment($"ViewChanged: VerticalOffset={scrollViewer.VerticalOffset}"); viewChanged.Set(); } }; var itemsSource = new ObservableCollection <ObservableCollection <int> >(); for (int i = 0; i < 100; i++) { itemsSource.Add(new ObservableCollection <int>(Enumerable.Range(0, 5))); } ; rootRepeater.ItemsSource = itemsSource; Content = anchorProvider; }); // scroll down several times to cause recycling of elements for (int i = 1; i < 10; i++) { IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Log.Comment($"Size=({rootRepeater.ActualWidth} x {rootRepeater.ActualHeight})"); Log.Comment($"ChangeView(VerticalOffset={i * 200})"); scrollViewer.ChangeView(null, i * 200, null, disableAnimation); }); Log.Comment("Waiting for view change completion..."); Verify.IsTrue(viewChanged.WaitOne(DefaultWaitTimeInMS)); viewChanged.Reset(); Log.Comment("View change completed"); RunOnUIThread.Execute(() => { Verify.AreEqual(i * 200, scrollViewer.VerticalOffset); }); } // } }
public void VerifyFocusedItemIsRecycledOnCollectionReset() { List <Layout> layouts = new List <Layout>(); RunOnUIThread.Execute(() => { layouts.Add(new MyCustomNonVirtualizingStackLayout()); layouts.Add(new StackLayout()); }); foreach (var layout in layouts) { List <string> items = new List <string> { "item0", "item1", "item2", "item3", "item4", "item5", "item6", "item7", "item8", "item9" }; const int targetIndex = 4; string targetItem = items[targetIndex]; ItemsRepeater repeater = null; RunOnUIThread.Execute(() => { repeater = new ItemsRepeater() { ItemsSource = items, ItemTemplate = CreateDataTemplateWithContent(@"<Button Content='{Binding}'/>"), Layout = layout }; Content = repeater; }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Log.Comment("Setting Focus on item " + targetIndex); Button toFocus = (Button)repeater.TryGetElement(targetIndex); Verify.AreEqual(targetItem, toFocus.Content as string); toFocus.Focus(); }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Log.Comment("Removing focused element from collection"); items.Remove(targetItem); Log.Comment("Reset the collection with an empty list"); repeater.ItemsSource = new List <string>(); }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Log.Comment("Verify new elements"); for (int i = 0; i < items.Count; i++) { Button currentButton = (Button)repeater.TryGetElement(i); Verify.IsNull(currentButton); } }); } }
public void VerifyCurrentAnchor() { if (PlatformConfiguration.IsDebugBuildConfiguration()) { // Test is failing in chk configuration due to: // Bug #1726 Test Failure: RepeaterTests.VerifyCurrentAnchor Log.Warning("Skipping test for Debug builds."); return; } ItemsRepeater rootRepeater = null; ScrollViewer scrollViewer = null; ItemsRepeaterScrollHost scrollhost = null; ManualResetEvent viewChanged = new ManualResetEvent(false); RunOnUIThread.Execute(() => { scrollhost = (ItemsRepeaterScrollHost)XamlReader.Parse( @"<controls:ItemsRepeaterScrollHost Width='400' Height='600' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:controls='http://schemas.modernwpf.com/2019'> <controls:ItemsRepeaterScrollHost.Resources> <DataTemplate x:Key='ItemTemplate' > <TextBlock Text='{Binding}' Height='50'/> </DataTemplate> </controls:ItemsRepeaterScrollHost.Resources> <ScrollViewer x:Name='scrollviewer'> <controls:ItemsRepeater x:Name='rootRepeater' ItemTemplate='{StaticResource ItemTemplate}' VerticalCacheLength='0' /> </ScrollViewer> </controls:ItemsRepeaterScrollHost>"); rootRepeater = (ItemsRepeater)scrollhost.FindName("rootRepeater"); scrollViewer = (ScrollViewer)scrollhost.FindName("scrollviewer"); scrollViewer.ScrollChanged += (sender, args) => { if (args.HorizontalChange != 0 || args.VerticalChange != 0) { viewChanged.Set(); } }; rootRepeater.ItemsSource = Enumerable.Range(0, 500); Content = scrollhost; }); // scroll down several times and validate current anchor for (int i = 1; i < 10; i++) { IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { scrollViewer.ChangeView(null, i * 200, null); }); Verify.IsTrue(viewChanged.WaitOne(DefaultWaitTimeInMS)); viewChanged.Reset(); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Verify.AreEqual(i * 200, scrollViewer.VerticalOffset); var anchor = scrollhost.CurrentAnchor; var anchorIndex = rootRepeater.GetElementIndex(anchor); Log.Comment("CurrentAnchor: " + anchorIndex); Verify.AreEqual(i * 4, anchorIndex); }); } }
public AnimationManager(ItemsRepeater owner) { m_owner = owner; // ItemsRepeater is not fully constructed yet. Don't interact with it. }
public void BringIntoViewOfExistingItemsDoesNotChangeScrollOffset() { System.Windows.Controls.ScrollViewerEx scrollViewer = null; ItemsRepeater repeater = null; AutoResetEvent scrollViewerScrolledEvent = new AutoResetEvent(false); RunOnUIThread.Execute(() => { repeater = new ItemsRepeater(); repeater.ItemsSource = Enumerable.Range(0, 100).Select(x => x.ToString()).ToList(); scrollViewer = new System.Windows.Controls.ScrollViewerEx() { Content = repeater, MaxHeight = 400, MaxWidth = 200 }; Content = scrollViewer; Content.UpdateLayout(); }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Log.Comment("Scroll to end"); scrollViewer.ViewChanged += (object sender, ScrollViewerViewChangedEventArgs e) => { if (!e.IsIntermediate) { Log.Comment("ScrollViewer scrolling finished"); scrollViewerScrolledEvent.Set(); } }; scrollViewer.ChangeView(null, repeater.ActualHeight, null); scrollViewer.UpdateLayout(); }); Log.Comment("Wait for scrolling"); if (Debugger.IsAttached) { scrollViewerScrolledEvent.WaitOne(); } else { if (!scrollViewerScrolledEvent.WaitOne(TimeSpan.FromMilliseconds(5000))) { throw new Exception("Timeout expiration in WaitForEvent."); } } IdleSynchronizer.Wait(); double endOfScrollOffset = 0; RunOnUIThread.Execute(() => { Log.Comment("Determine scrolled offset"); endOfScrollOffset = scrollViewer.VerticalOffset; // Idea: we might not have scrolled to the end, however we should at least have moved so much that the end is not too far away Verify.IsTrue(Math.Abs(endOfScrollOffset - repeater.ActualHeight) < 500, $"We should at least have scrolled some amount. " + $"ScrollOffset:{endOfScrollOffset} Repeater height: {repeater.ActualHeight}"); var lastItem = (FrameworkElement)repeater.GetOrCreateElement(99); lastItem.UpdateLayout(); Log.Comment("Bring last element into view"); lastItem.BringIntoView(); }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Log.Comment("Verify position did not change"); Verify.IsTrue(Math.Abs(endOfScrollOffset - scrollViewer.VerticalOffset) < 1); }); }
public UniqueIdElementPool(ItemsRepeater owner) { m_owner = owner; // ItemsRepeater is not fully constructed yet. Don't interact with it. }
public PinnedElementInfo(UIElement element) { PinnedElement = element; VirtualizationInfo = ItemsRepeater.GetVirtualizationInfo(element); }
private UIElement GetElementFromElementFactory(int index) { // The view generator is the provider of last resort. var data = m_owner.ItemsSourceView.GetAt(index); var itemTemplateFactory = m_owner.ItemTemplateShim; UIElement element = null; bool itemsSourceContainsElements = false; if (itemTemplateFactory == null) { element = data as UIElement; // No item template provided and ItemsSource contains objects derived from UIElement. // In this case, just use the data directly as elements. itemsSourceContainsElements = element != null; } if (element == null) { if (itemTemplateFactory == null) { // If no ItemTemplate was provided, use a default var factory = XamlReader.Parse("<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'><TextBlock Text='{Binding}'/></DataTemplate>") as DataTemplate; m_owner.ItemTemplate = factory; itemTemplateFactory = m_owner.ItemTemplateShim; } if (m_ElementFactoryGetArgs == null) { // Create one. m_ElementFactoryGetArgs = new ElementFactoryGetArgs(); } var args = m_ElementFactoryGetArgs; args.Data = data; args.Parent = m_owner; args.Index = index; element = itemTemplateFactory.GetElement(args); args.Data = null; args.Parent = null; } var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) { virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); } else { // View obtained from ElementFactory already has a VirtualizationInfo attached to it // which means that the element has been recycled and not created from scratch. } if (!itemsSourceContainsElements) { // Set data context only if no x:Bind was used. ie. No data template component on the root. var elementAsFE = element as FrameworkElement; elementAsFE.DataContext = data; } virtInfo.MoveOwnershipToLayoutFromElementFactory( index, /* uniqueId: */ m_owner.ItemsSourceView.HasKeyIndexMapping ? m_owner.ItemsSourceView.KeyFromIndex(index) : string.Empty); // The view generator is the only provider that prepares the element. var repeater = m_owner; // Add the element to the children collection here before raising OnElementPrepared so // that handlers can walk up the tree in case they want to find their IndexPath in the // nested case. var children = repeater.Children; if (CachedVisualTreeHelpers.GetParent(element) != repeater) { children.Add(element); } repeater.AnimationManager.OnElementPrepared(element); repeater.OnElementPrepared(element, index); // Update realized indices m_firstRealizedElementIndexHeldByLayout = Math.Min(m_firstRealizedElementIndexHeldByLayout, index); m_lastRealizedElementIndexHeldByLayout = Math.Max(m_lastRealizedElementIndexHeldByLayout, index); return(element); }
public void ValidateNonVirtualLayoutDoesNotGetMeasuredForViewportChanges() { RunOnUIThread.Execute(() => { int measureCount = 0; int arrangeCount = 0; var repeater = new ItemsRepeater(); // with a non virtualizing layout, repeater will just // run layout once. repeater.Layout = new MockNonVirtualizingLayout() { MeasureLayoutFunc = (size, context) => { measureCount++; return(new Size(100, 800)); }, ArrangeLayoutFunc = (size, context) => { arrangeCount++; return(new Size(100, 800)); } }; repeater.ItemsSource = Enumerable.Range(0, 10); repeater.ItemTemplate = (DataTemplate)XamlReader.Parse( @"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'> <Button Content='{Binding}' Height='100' /> </DataTemplate>"); Content = new ScrollViewer() { Content = repeater }; Content.UpdateLayout(); Verify.AreEqual(1, measureCount); Verify.AreEqual(1, arrangeCount); measureCount = 0; arrangeCount = 0; // Once we switch to a virtualizing layout we should // get at least two passes to update the viewport. repeater.Layout = new MockVirtualizingLayout() { MeasureLayoutFunc = (size, context) => { measureCount++; return(new Size(100, 800)); }, ArrangeLayoutFunc = (size, context) => { arrangeCount++; return(new Size(100, 800)); } }; Content.UpdateLayout(true); Verify.IsGreaterThan(measureCount, 1); Verify.IsGreaterThan(arrangeCount, 1); }); }
public void OnItemsSourceChanged(object source, NotifyCollectionChangedEventArgs args) { // Note: For items that have been removed, the index will not be touched. It will hold // the old index before it was removed. It is not valid anymore. switch (args.Action) { case NotifyCollectionChangedAction.Add: { var newIndex = args.NewStartingIndex; var newCount = args.NewItems.Count; EnsureFirstLastRealizedIndices(); if (newIndex <= m_lastRealizedElementIndexHeldByLayout) { m_lastRealizedElementIndexHeldByLayout += newCount; var children = m_owner.Children; var childCount = children.Count; for (int i = 0; i < childCount; ++i) { var element = children[i]; var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var dataIndex = virtInfo.Index; if (virtInfo.IsRealized && dataIndex >= newIndex) { UpdateElementIndex(element, virtInfo, dataIndex + newCount); } } } else { // Indices held by layout are not affected // We could still have items in the pinned elements that need updates. This is usually a very small vector. for (int i = 0; i < m_pinnedPool.Count; ++i) { var elementInfo = m_pinnedPool[i]; var virtInfo = elementInfo.VirtualizationInfo; var dataIndex = virtInfo.Index; if (virtInfo.IsRealized && dataIndex >= newIndex) { var element = elementInfo.PinnedElement; UpdateElementIndex(element, virtInfo, dataIndex + newCount); } } } break; } case NotifyCollectionChangedAction.Replace: { // Requirement: oldStartIndex == newStartIndex. It is not a replace if this is not true. // Two cases here // case 1: oldCount == newCount // indices are not affected. nothing to do here. // case 2: oldCount != newCount // Replaced with less or more items. This is like an insert or remove // depending on the counts. var oldStartIndex = args.OldStartingIndex; var newStartingIndex = args.NewStartingIndex; var oldCount = args.OldItems.Count; var newCount = args.NewItems.Count; if (oldStartIndex != newStartingIndex) { throw new Exception("Replace is only allowed with OldStartingIndex equals to NewStartingIndex."); } if (oldCount == 0) { throw new Exception("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead."); } if (newCount == 0) { throw new Exception("Replace notification with args.NewItemCount value of 0 is not allowed. Use Remove action instead."); } int countChange = newCount - oldCount; if (countChange != 0) { // countChange > 0 : countChange items were added // countChange < 0 : -countChange items were removed var children = m_owner.Children; for (int i = 0; i < children.Count; ++i) { var element = children[i]; var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var dataIndex = virtInfo.Index; if (virtInfo.IsRealized) { if (dataIndex >= oldStartIndex + oldCount) { UpdateElementIndex(element, virtInfo, dataIndex + countChange); } } } EnsureFirstLastRealizedIndices(); m_lastRealizedElementIndexHeldByLayout += countChange; } break; } case NotifyCollectionChangedAction.Remove: { var oldStartIndex = args.OldStartingIndex; var oldCount = args.OldItems.Count; var children = m_owner.Children; for (int i = 0; i < children.Count; ++i) { var element = children[i]; var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); var dataIndex = virtInfo.Index; if (virtInfo.IsRealized) { if (virtInfo.AutoRecycleCandidate && oldStartIndex <= dataIndex && dataIndex < oldStartIndex + oldCount) { // If we are doing the mapping, remove the element who's data was removed. m_owner.ClearElementImpl(element); } else if (dataIndex >= (oldStartIndex + oldCount)) { UpdateElementIndex(element, virtInfo, dataIndex - oldCount); } } } InvalidateRealizedIndicesHeldByLayout(); break; } case NotifyCollectionChangedAction.Reset: { // If we get multiple resets back to back before // running layout, we dont have to clear all the elements again. if (!m_isDataSourceStableResetPending) { #if DEBUG // There should be no elements in the reset pool at this time. Debug.Assert(m_resetPool.IsEmpty); #endif if (m_owner.ItemsSourceView.HasKeyIndexMapping) { m_isDataSourceStableResetPending = true; } // Walk through all the elements and make sure they are cleared, they will go into // the stable id reset pool. var children = m_owner.Children; for (int i = 0; i < children.Count; ++i) { var element = children[i]; var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate) { m_owner.ClearElementImpl(element); } } } InvalidateRealizedIndicesHeldByLayout(); break; } } }
public ViewManager(ItemsRepeater owner) { m_owner = owner; m_resetPool = new UniqueIdElementPool(owner); // ItemsRepeater is not fully constructed yet. Don't interact with it. }
private UIElement FindFocusCandidate(int clearedIndex, ref UIElement focusedChild) { // Walk through all the children and find elements with index before and after the cleared index. // Note that during a delete the next element would now have the same index. int previousIndex = int.MinValue; int nextIndex = int.MaxValue; UIElement nextElement = null; UIElement previousElement = null; var children = m_owner.Children; for (int i = 0; i < children.Count; ++i) { var child = children[i]; var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(child); if (virtInfo != null && virtInfo.IsHeldByLayout) { int currentIndex = virtInfo.Index; if (currentIndex < clearedIndex) { if (currentIndex > previousIndex) { previousIndex = currentIndex; previousElement = child; } } else if (currentIndex >= clearedIndex) { // Note that we use >= above because if we deleted the focused element, // the next element would have the same index now. if (currentIndex < nextIndex) { nextIndex = currentIndex; nextElement = child; } } } } // Find the next element if one exists, if not use the previous element. // If the container itself is not focusable, find a descendent that is. UIElement focusCandidate = null; if (nextElement != null) { focusedChild = nextElement as UIElement; if (nextElement.Focus()) { focusCandidate = nextElement; } else if (nextElement.MoveFocus(new TraversalRequest(FocusNavigationDirection.First))) { focusCandidate = FocusManager.GetFocusedElement(nextElement) as UIElement; } } if (focusCandidate == null && previousElement != null) { focusedChild = previousElement as UIElement; if (previousElement.Focus()) { focusCandidate = previousElement; } else if (previousElement.MoveFocus(new TraversalRequest(FocusNavigationDirection.Last))) { focusCandidate = FocusManager.GetFocusedElement(previousElement) as UIElement; } } return(focusCandidate); }
public RepeaterLayoutContext(ItemsRepeater owner) { m_owner = new WeakReference <ItemsRepeater>(owner); }