public void ValidateDataContextGetsPropagated() { const string c_element1DataContext = "Element1_DataContext"; RunOnUIThread.Execute(() => { var data = new List <Button>() { new Button() { Content = "Element1_Content", DataContext = c_element1DataContext } }; var elementFactory = new ElementFromElementElementFactory(); var repeater = new ItemsRepeater() { ItemsSource = data, ItemTemplate = elementFactory }; Content = repeater; Content.UpdateLayout(); // Verify that DataContext of data has propagated to the container var firstElement = repeater.TryGetElement(0) as Button; var retrievedDataContextItem1 = firstElement.DataContext as string; Verify.IsTrue(data[0] == firstElement.Content); Verify.IsTrue(retrievedDataContextItem1 == c_element1DataContext); }); }
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 ValidateDataContextDoesNotGetOverwritten() { const string c_element1DataContext = "Element1_DataContext"; RunOnUIThread.Execute(() => { var data = new List <Button>() { new Button() { Content = "Element1_Content", DataContext = c_element1DataContext } }; var elementFactory = new DataAsElementElementFactory(); var repeater = new ItemsRepeater() { ItemsSource = data, ItemTemplate = elementFactory }; Content = repeater; Content.UpdateLayout(); // Verify that DataContext is still the same var firstElement = repeater.TryGetElement(0) as Button; var retrievedDataContextItem1 = firstElement.DataContext as string; Verify.IsTrue(retrievedDataContextItem1 == c_element1DataContext); }); }
private void SetupRepeater(IList data, bool phased) { var repeater = new ItemsRepeater(); repeater.ItemsSource = data; if (phased) { repeater.ItemTemplate = (ElementFactory)Resources["SharedElementFactoryPhased"]; } else { repeater.ItemTemplate = (ElementFactory)Resources["SharedElementFactoryBinding"]; } if (data is IList <Recipe> ) { repeater.Layout = (VirtualizingLayout)Resources["SharedFlowLayout"]; } else { repeater.Layout = (VirtualizingLayout)Resources["SharedStackLayout"]; } repeater.VerticalCacheLength = 4.0; repeater.XYFocusKeyboardNavigation = Windows.UI.Xaml.Input.XYFocusKeyboardNavigationMode.Enabled; var tracker = new ItemsRepeaterScrollHost(); var scrollViewer = new Windows.UI.Xaml.Controls.ScrollViewer(); tracker.ScrollViewer = scrollViewer; scrollViewer.Content = repeater; scrollViewer.IsTabStop = false; host.Child = tracker; _printChildrenCount = () => VisualTreeHelper.GetChildrenCount(repeater).ToString(); }
// [TestMethod] Issue 1018 public void ValidateFocusMoveOnElementClearedWithUniqueIds() { ItemsRepeater repeater = null; CustomItemsSource dataSource = null; RunOnUIThread.Execute(() => { dataSource = new CustomItemsSourceWithUniqueId(Enumerable.Range(0, 5).ToList()); }); repeater = SetupRepeater(dataSource, "<Button Content='{Binding}' Height='10' />"); // dataSource: 0 1 2 3 4 // Index 0 deleted, focus should be on the new element which has index 0 SharedHelpers.RunActionsWithWait( new Action[] { () => { MoveFocusToIndex(repeater, 0); }, () => { dataSource.Remove(0 /* index */, 1 /* count */, true /* reset*/); }, () => { ValidateCurrentFocus(repeater, 0 /*expectedIndex */, "1" /* expectedContent */); } }); // dataSource: 1 2 3 4 // Last element deleted, focus should move to the previous element int lastIndex = dataSource.Inner.Count - 1; SharedHelpers.RunActionsWithWait( new Action[] { () => { MoveFocusToIndex(repeater, lastIndex); }, () => { dataSource.Remove(lastIndex /* index */, 1 /* count */, true /* reset*/); }, () => { ValidateCurrentFocus(repeater, 2 /*expectedIndex */, "3" /* expectedContent */); } }); // dataSource: 1 2 3 // Reset should keep the focused element as long as the unique id matches. SharedHelpers.RunActionsWithWait( new Action[] { () => { MoveFocusToIndex(repeater, 0); }, () => { dataSource.Reset(); }, () => { int newIndex = dataSource.Inner.IndexOf(1); ValidateCurrentFocus(repeater, newIndex /*expectedIndex */, "1" /* expectedContent */); } }); // dataSource: 1 2 3 // Remove multiple elements SharedHelpers.RunActionsWithWait( new Action[] { () => { MoveFocusToIndex(repeater, 0); }, () => { dataSource.Remove(0 /* index */, 2 /* count */, true /* reset*/); }, () => { ValidateCurrentFocus(repeater, 0 /*expectedIndex */, "3" /* expectedContent */); } }); }
private void ValidateCurrentFocus(ItemsRepeater repeater, int expectedIndex, string expectedContent) { var currentFocus = FocusManager.GetFocusedElement() as ContentControl; var currentFocusedIndex = repeater.GetElementIndex(currentFocus); Log.Comment("expectedIndex: " + expectedIndex + " actual : " + currentFocusedIndex); Verify.AreEqual(expectedIndex, currentFocusedIndex); Log.Comment("expectedContent: " + expectedContent + " actual : " + currentFocus.Content.ToString()); Verify.AreEqual(expectedContent, currentFocus.Content.ToString()); }
public void ValidateGetSetItemsSource() { RunOnUIThread.Execute(() => { ItemsRepeater repeater = new ItemsRepeater(); var dataSource = new InspectingDataSource(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); }); }
private ItemsRepeater CreateRepeater(object dataSource, object elementFactory) { var repeater = new ItemsRepeater { ItemsSource = dataSource, ItemTemplate = elementFactory }; repeater.Layout = CreateLayout(repeater); return(repeater); }
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.Load( @"<controls:ItemsRepeaterScrollHost xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='using:MUXControlsTestApp.Samples' xmlns:controls='using:Microsoft.UI.Xaml.Controls'> <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.Load( @"<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 static ImplicitAnimationCollection CreateReorderAnimationCollection(MUXC.ItemsRepeater itemsRepeater) { Compositor compositor = ElementCompositionPreview.GetElementVisual(itemsRepeater).Compositor; Vector3KeyFrameAnimation offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); offsetAnimation.Duration = TimeSpan.FromMilliseconds(ReorderAnimationDuration); offsetAnimation.Target = nameof(Visual.Offset); CompositionAnimationGroup animationGroup = compositor.CreateAnimationGroup(); animationGroup.Add(offsetAnimation); ImplicitAnimationCollection animationCollection = compositor.CreateImplicitAnimationCollection(); animationCollection[nameof(Visual.Offset)] = animationGroup; return(animationCollection); }
public void CanResetLayoutAfterUniqueIdReset() { var data = new WinRTCollection(Enumerable.Range(0, 2).Select(i => string.Format("Item #{0}", i))); object dataSource = null; RunOnUIThread.Execute(() => dataSource = MockItemsSource.CreateDataSource(data, supportsUniqueIds: true)); ItemsRepeater repeater = SetupRepeater(dataSource); RunOnUIThread.Execute(() => { var range = new UIElement[] { repeater.TryGetElement(0), repeater.TryGetElement(1) }; var clearedElements = new List <UIElement>(); repeater.ElementClearing += (s, e) => { clearedElements.Add(e.Element); }; // The realized elements will be sent to the unique id reset pool. // They haven't been cleared yet. data.Reset(); Verify.AreEqual(0, clearedElements.Count); // This also cause elements to be sent to the unique id reset pool. // We are validating here that we are smart enough not send them there twice. // Doing so will cause an exception to be thrown. repeater.Layout = null; Verify.AreEqual(0, clearedElements.Count); repeater.UpdateLayout(); // Layout runs. The elements in the reset pool are not used. // They should be cleared back to the view generator at this point. Verify.AreEqual(2, clearedElements.Count); Verify.AreEqual(range[0], clearedElements[0]); Verify.AreEqual(range[1], clearedElements[1]); Verify.IsNull(repeater.TryGetElement(0)); Verify.IsNull(repeater.TryGetElement(1)); }); }
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; } }); }
public void CanChangeFocusAfterUniqueIdReset() { var data = new WinRTCollection(Enumerable.Range(0, 2).Select(i => string.Format("Item #{0}", i))); object dataSource = null; RunOnUIThread.Execute(() => dataSource = MockItemsSource.CreateDataSource(data, supportsUniqueIds: true)); ItemsRepeater repeater = SetupRepeater(dataSource); Control focusedElement = null; RunOnUIThread.Execute(() => { focusedElement = (Control)repeater.TryGetElement(0); focusedElement.Focus(FocusState.Keyboard); }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { data.Reset(); }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { // Still focused. Verify.AreEqual(focusedElement, FocusManager.GetFocusedElement()); // Change focused element. focusedElement = (Control)repeater.TryGetElement(1); focusedElement.Focus(FocusState.Keyboard); }); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { // Focus is on the new element. Verify.AreEqual(focusedElement, FocusManager.GetFocusedElement()); }); }
// Why does this test work? // When the elements get created from the RecyclingElementFactory, we get already "existing" data templates. // However, the reason for the crash in #2384 is that those "empty" data templates actually still had their data context // If that data context is not null, that means it did not get cleared when the element was recycled, which is the wrong behavior. // To check if the clearing is working correctly, we are checking this inside the ElementFactory's RecycleElement function. public void ValidateElementClearingClearsDataContext() { ItemsRepeater repeater = null; MockElementFactory elementFactory = null; int elementClearingRaisedCount = 0; Log.Comment("Initialize ItemsRepeater"); RunOnUIThread.Execute(() => { elementFactory = new MockElementFactory() { GetElementFunc = delegate(int index, UIElement owner) { return(new Button() { Content = index }); }, ClearElementFunc = delegate(UIElement element, UIElement owner) { elementClearingRaisedCount++; Verify.IsNull((element as FrameworkElement).DataContext); } }; repeater = CreateRepeater(Enumerable.Range(0, 100), elementFactory); repeater.Layout = new StackLayout(); Content = repeater; repeater.UpdateLayout(); repeater.ItemsSource = null; Log.Comment("Verify ItemsRepeater cleared data contexts correctly"); Verify.IsTrue(elementClearingRaisedCount > 0, "ItemsRepeater should have cleared some elements"); }); }
private ItemsRepeater SetupRepeater(object dataSource, VirtualizingLayout layout, string itemContent, out ScrollViewer scrollViewer) { ItemsRepeater repeater = null; ScrollViewer sv = null; RunOnUIThread.Execute(() => { var elementFactory = new RecyclingElementFactory(); elementFactory.RecyclePool = new RecyclePool(); elementFactory.Templates["Item"] = (DataTemplate)XamlReader.Load( @"<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'> " + itemContent + @"</DataTemplate>"); repeater = new ItemsRepeater() { ItemsSource = dataSource, ItemTemplate = elementFactory, Layout = layout, HorizontalCacheLength = 0.0, VerticalCacheLength = 0.0 }; sv = new ScrollViewer { Content = repeater }; Content = new ItemsRepeaterScrollHost() { Width = 200, Height = 200, ScrollViewer = sv }; }); IdleSynchronizer.Wait(); scrollViewer = sv; return(repeater); }
private VirtualizingLayout CreateLayout(ItemsRepeater repeater) { var layout = new MockVirtualizingLayout(); var children = new List <UIElement>(); layout.MeasureLayoutFunc = (availableSize, context) => { repeater.Tag = repeater.Tag ?? context; children.Clear(); var itemCount = context.ItemCount; for (int i = 0; i < itemCount; ++i) { var element = context.GetOrCreateElementAt(i); element.Measure(availableSize); children.Add(element); } return(new Size(10, 10)); }; return(layout); }
public ViewportManagerWithPlatformFeatures(ItemsRepeater owner) { // ItemsRepeater is not fully constructed yet. Don't interact with it. m_owner = owner; }
public void VerifyStoreScenarioCache() { ItemsRepeater rootRepeater = null; RunOnUIThread.Execute(() => { var scrollhost = (ItemsRepeaterScrollHost)XamlReader.Load( @" <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='using:Microsoft.UI.Xaml.Controls'> <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 HorizontalScrollMode='Enabled' VerticalScrollMode='Disabled' 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)); }); }
// [TestMethod] Issue #1018 public void CanPinFocusedElements() { // Setup a grouped repeater scenario with two groups each containing two items. var data = new ObservableCollection <ObservableCollection <string> >(Enumerable .Range(0, 2) .Select(i => new ObservableCollection <string>(Enumerable .Range(0, 2) .Select(j => string.Format("Item #{0}.{1}", i, j))))); List <ContentControl>[] itemElements = null; ItemsRepeater[] innerRepeaters = null; List <StackPanel> groupElements = null; ItemsRepeater rootRepeater = null; var gotFocus = new ManualResetEvent(false); RunOnUIThread.Execute(() => { itemElements = new[] { Enumerable.Range(0, 2).Select(i => new ContentControl()).ToList(), Enumerable.Range(0, 2).Select(i => new ContentControl()).ToList() }; itemElements[0][0].GotFocus += delegate { gotFocus.Set(); }; innerRepeaters = Enumerable.Range(0, 2).Select(i => CreateRepeater( MockItemsSource.CreateDataSource(data[i], supportsUniqueIds: false), MockElementFactory.CreateElementFactory(itemElements[i]))).ToArray(); groupElements = Enumerable.Range(0, 2).Select(i => { var panel = new StackPanel(); panel.Children.Add(new ContentControl()); panel.Children.Add(innerRepeaters[i]); return(panel); }).ToList(); rootRepeater = CreateRepeater( MockItemsSource.CreateDataSource(data, supportsUniqueIds: false), MockElementFactory.CreateElementFactory(groupElements)); Content = rootRepeater; rootRepeater.UpdateLayout(); itemElements[0][0].Focus(FocusState.Keyboard); }); Verify.IsTrue(gotFocus.WaitOne(DefaultWaitTimeInMS), "Waiting for focus event on the first element of the first group."); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Log.Comment("Recycle focused element 0.0 and validate it's still realized because it is pinned."); { var ctx = (VirtualizingLayoutContext)innerRepeaters[0].Tag; ctx.RecycleElement(itemElements[0][0]); Verify.AreEqual(0, innerRepeaters[0].GetElementIndex(itemElements[0][0])); } Log.Comment("Recycle element 0.1 and validate it's no longer realized because it is not pinned."); { var ctx = (VirtualizingLayoutContext)innerRepeaters[0].Tag; ctx.RecycleElement(itemElements[0][1]); Verify.AreEqual(-1, innerRepeaters[0].GetElementIndex(itemElements[0][1])); } Log.Comment("Recycle group 0 and validate it's still realized because one of its items is pinned."); { var ctx = (VirtualizingLayoutContext)rootRepeater.Tag; ctx.RecycleElement(groupElements[0]); Verify.AreEqual(0, rootRepeater.GetElementIndex(groupElements[0])); } itemElements[1][1].GotFocus += delegate { gotFocus.Set(); }; itemElements[1][1].Focus(FocusState.Keyboard); }); Verify.IsTrue(gotFocus.WaitOne(DefaultWaitTimeInMS), "Waiting for focus event on the second element of the second group."); IdleSynchronizer.Wait(); RunOnUIThread.Execute(() => { Log.Comment(@"Move focus to item 1.1 and validate item 0.0 and group 0 are recycled because the only thing keeping them around is the fact that item 0.0 was focus pinned" ); { ((VirtualizingLayoutContext)rootRepeater.Tag).RecycleElement(groupElements[0]); ((VirtualizingLayoutContext)innerRepeaters[0].Tag).RecycleElement(itemElements[0][0]); Verify.AreEqual(-1, rootRepeater.GetElementIndex(groupElements[0])); Verify.AreEqual(-1, innerRepeaters[0].GetElementIndex(itemElements[0][0])); Verify.AreEqual(1, innerRepeaters[0].GetElementIndex(itemElements[1][1])); } Log.Comment(@"Delete item 1.1 from the data. This will force the element to get recycled even if it's pinned."); { data[1].RemoveAt(1); rootRepeater.UpdateLayout(); Verify.AreEqual(-1, innerRepeaters[1].GetElementIndex(itemElements[1][1])); } }); }
public void VerifyCorrectionsInNonScrollableDirection() { ItemsRepeater rootRepeater = null; ScrollViewer scrollViewer = null; ItemsRepeaterScrollHost scrollhost = null; ManualResetEvent viewChanged = new ManualResetEvent(false); RunOnUIThread.Execute(() => { scrollhost = (ItemsRepeaterScrollHost)XamlReader.Load( @"<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='using:Microsoft.UI.Xaml.Controls'> <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.ViewChanged += (sender, args) => { if (!args.IsIntermediate) { 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(); } }
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(FocusState.Keyboard); }); 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); } }); } }
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.Load( @"<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='using:Microsoft.UI.Xaml.Controls'> <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.ViewChanged += (sender, args) => { Log.Comment($"ViewChanged: VerticalOffset={scrollViewer.VerticalOffset}, IsIntermediate={args.IsIntermediate}"); if (!args.IsIntermediate) { 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 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.Load( @"<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='using:Microsoft.UI.Xaml.Controls'> <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.ViewChanged += (sender, args) => { if (!args.IsIntermediate) { 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 = PlatformConfiguration.IsOSVersionLessThan(OSVersion.Redstone5) ? scrollhost.CurrentAnchor : scrollViewer.CurrentAnchor; var anchorIndex = rootRepeater.GetElementIndex(anchor); Log.Comment("CurrentAnchor: " + anchorIndex); Verify.AreEqual(i * 4, anchorIndex); }); } }
Control FindFocusCandidate(int clearedIndex, 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. Control focusCandidate = null; if (nextElement != null) { //focusedChild = nextElement as UIElement; focusCandidate = nextElement as Control; if (focusCandidate == null) { var firstFocus = FocusManager.FindFirstFocusableElement(nextElement); if (firstFocus != null) { focusCandidate = firstFocus as Control; } } } // TODO UNO: Case declared as useless by intellisense //if (focusCandidate == null && previousElement != null) //{ // focusedChild = previousElement as UIElement; // focusCandidate = previousElement as Control; // if (previousElement == null) // { // var lastFocus = FocusManager.FindLastFocusableElement(previousElement); // if (lastFocus != null) // { // focusCandidate = lastFocus as Control; // } // } //} return(focusCandidate); }
public PinnedElementInfo(UIElement element) { m_pinnedElement = element; m_virtInfo = ItemsRepeater.GetVirtualizationInfo(element); }
public void OnItemsSourceChanged(object _, 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 = (int)(args.OldItems.Count); var newCount = (int)(args.NewItems.Count); if (oldStartIndex != newStartingIndex) { throw new InvalidOperationException("Replace is only allowed with OldStartingIndex equals to NewStartingIndex."); } if (oldCount == 0) { throw new InvalidOperationException("Replace notification with args.OldItemsCount value of 0 is not allowed. Use Insert action instead."); } if (newCount == 0) { throw new InvalidOperationException("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 = (int)(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. MUX_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; } }
// There are several cases handled here with respect to which element gets returned and when DataContext is modified. // // 1. If there is no ItemTemplate: // 1.1 If data is a UIElement . the data is returned // 1.2 If data is not a UIElement . a default DataTemplate is used to fetch element and DataContext is set to data** // // 2. If there is an ItemTemplate: // 2.1 If data is not a FrameworkElement . Element is fetched from ElementFactory and DataContext is set to the data** // 2.2 If data is a FrameworkElement: // 2.2.1 If Element returned by the ElementFactory is the same as the data . Element (a.k.a. data) is returned as is // 2.2.2 If Element returned by the ElementFactory is not the same as the data // . Element that is fetched from the ElementFactory is returned and // DataContext is set to the data's DataContext (if it exists), otherwise it is set to the data itself** // // **data context is set only if no x:Bind was used. ie. No data template component on the root. UIElement GetElementFromElementFactory(int index) { // The view generator is the provider of last resort. var data = m_owner.ItemsSourceView.GetAt(index); UIElement GetElement() { var elementFactory = m_owner.ItemTemplateShim; if (elementFactory == null) { if (data is UIElement dataAsElement) { return(dataAsElement); } else { // If no ItemTemplate was provided, use a default //var factory = XamlReader.Load("<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'><TextBlock Text='{Binding}'/></DataTemplate>") as DataTemplate; var factory = new DataTemplate(() => { var tb = new TextBlock(); tb.SetBinding(TextBlock.TextProperty, new Binding()); return(tb); }); m_owner.ItemTemplate = factory; elementFactory = m_owner.ItemTemplateShim; } } if (m_ElementFactoryGetArgs == null) { m_ElementFactoryGetArgs = new ElementFactoryGetArgs(); } var args = m_ElementFactoryGetArgs; using var scopeGuard = Disposable.Create(() => { args.Data = null; args.Parent = null; }); args.Data = data; args.Parent = m_owner; args.Index = index; return(elementFactory.GetElement(args)); }; var element = GetElement(); var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); if (virtInfo == null) { virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); REPEATER_TRACE_PERF("ElementCreated"); } 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. REPEATER_TRACE_PERF("ElementRecycled"); } if (data != element) { // Prepare the element // If we are phasing, run phase 0 before setting DataContext. If phase 0 is not // run before setting DataContext, when setting DataContext all the phases will be // run in the OnDataContextChanged handler in code generated by the xaml compiler (code-gen). var extension = CachedVisualTreeHelpers.GetDataTemplateComponent(element); if (extension != null) { // Clear out old data. extension.Recycle(); int nextPhase = VirtualizationInfo.PhaseReachedEnd; // Run Phase 0 extension.ProcessBindings(data, index, 0 /* currentPhase */, out nextPhase); // Setup phasing information, so that Phaser can pick up any pending phases left. // Update phase on virtInfo. Set data and templateComponent only if x:Phase was used. virtInfo.UpdatePhasingInfo(nextPhase, nextPhase > 0 ? data : null, nextPhase > 0 ? extension : null); } else 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. var elementDataContext = data; if (data is FrameworkElement dataAsElement) { var dataDataContext = dataAsElement.DataContext; if (dataDataContext != null) { elementDataContext = dataDataContext; } } elementAsFE.DataContext = elementDataContext; } else { MUX_ASSERT(false, "Element returned by factory is not a FrameworkElement!"); } } virtInfo.MoveOwnershipToLayoutFromElementFactory( index, /* uniqueId: */ m_owner.ItemsSourceView.HasKeyIndexMapping ? m_owner.ItemsSourceView.KeyFromIndex(index) : null); // The view generator is the only provider that prepares the element. var repeater = m_owner; #if IS_UNO //TODO: Uno specific - remove when #4689 is fixed repeater.OnUnoBeforeElementPrepared(element, index); #endif // 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); if (data != element) { m_phaser.PhaseElement(element, virtInfo); } // Update realized indices m_firstRealizedElementIndexHeldByLayout = Math.Min(m_firstRealizedElementIndexHeldByLayout, index); m_lastRealizedElementIndexHeldByLayout = Math.Max(m_lastRealizedElementIndexHeldByLayout, index); return(element); }