Forcing WPF to create items in ItemsControl

I want to check if the items in my ListBox are displayed correctly in the user interface. I decided that one way to do this is to go through all the children of the ListBox in the visual tree, get their text, and then compare this with what I expect from the text.

The problem with this approach is that inside the ListBox , VirtualizingStackPanel used to display its elements, so only elements that are visible are created. In the end, I came across the ItemContainerGenerator class, which seems to cause WPF to create controls in the visual tree for the specified item. Unfortunately, this causes some weird side effects for me. Here is my code for creating all the items in a ListBox :

 List<string> generatedItems = new List<string>(); IItemContainerGenerator generator = this.ItemsListBox.ItemContainerGenerator; GeneratorPosition pos = generator.GeneratorPositionFromIndex(-1); using(generator.StartAt(pos, GeneratorDirection.Forward)) { bool isNewlyRealized; for(int i = 0; i < this.ItemsListBox.Items.Count; i++) { isNewlyRealized = false; DependencyObject cntr = generator.GenerateNext(out isNewlyRealized); if(isNewlyRealized) { generator.PrepareItemContainer(cntr); } string itemText = GetControlText(cntr); generatedItems.Add(itemText); } } 

(I can provide the code for GetItemText() if you want, but it just traverses the visual tree until a TextBlock is found. I understand that these are other ways to have text in the element, but I will fix it as soon as I get the right one creating an item.)

In my application, an ItemsListBox contains 20 elements, of which the first 12 elements are initially visible. The text for the first 14 points is correct (probably because their controls are already created). However, for paragraphs 15-20, I do not get any text at all. Also, if I scroll to the bottom of the ItemsListBox , the text of items 15-20 is also empty. So it seems like I'm interfering with the normal WPF mechanism for generating controls in some way.

What am I doing wrong? Is there another / better way to force items to be added to ItemsControl in the visual tree?

Update . I think I found why this is happening, although I do not know how to fix it. My guess is that calling PrepareItemContainer() will generate any necessary controls to display the item, and then add the container to the visual tree in the right place. Turns out he doesn't do any of these things. The container is not added to the ItemsControl until I scroll down to view it, and at that time only the container itself is created (i.e. ListBoxItem ) - its children are not created (there should be several controls, one of which should be a TextBlock which will display the text of the element).

If I go through the visual tree of the control that I passed to PrepareItemContainer() , the results will be the same. In both cases, only ListBoxItem is created, and none of its children are created.

I could not find a good way to add ListBoxItem to the visual tree. I found VirtualizingStackPanel in the visual tree, but calling its Children.Add() InvalidOperationException (cannot add items directly to the ItemPanel , since it generates items for its ItemsControl ). Just like a test, I tried calling it AddVisualChild() with Reflection (since it is protected), but that didn't work either.

+6
wpf itemscontrol
source share
6 answers

I think I figured out how to do this. The problem was that the generated elements were not added to the visual tree. After some searching, I could come up with some protected VirtualizingStackPanel methods in a ListBox . Although this is not ideal, as it is only for testing, I think I will have to live with it.

This is what worked for me:

 VirtualizingStackPanel itemsPanel = null; FrameworkElementFactory factory = control.ItemsPanel.VisualTree; if(null != factory) { // This method traverses the visual tree, searching for a control of // the specified type and name. itemsPanel = FindNamedDescendantOfType(control, factory.Type, null) as VirtualizingStackPanel; } List<string> generatedItems = new List<string>(); IItemContainerGenerator generator = this.ItemsListBox.ItemContainerGenerator; GeneratorPosition pos = generator.GeneratorPositionFromIndex(-1); using(generator.StartAt(pos, GeneratorDirection.Forward)) { bool isNewlyRealized; for(int i = 0; i < this.ItemsListBox.Items.Count; i++) { isNewlyRealized = false; UIElement cntr = generator.GenerateNext(out isNewlyRealized) as UIElement; if(isNewlyRealized) { if(i >= itemsPanel.Children.Count) { itemsPanel.GetType().InvokeMember("AddInternalChild", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMember, Type.DefaultBinder, itemsPanel, new object[] { cntr }); } else { itemsPanel.GetType().InvokeMember("InsertInternalChild", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMember, Type.DefaultBinder, itemsPanel, new object[] { i, cntr }); } generator.PrepareItemContainer(cntr); } string itemText = GetControlText(cntr); generatedItems.Add(itemText); } } 
+1
source share

Just a quick search if the ListBox uses VirtualizingStackPanel - maybe this will be enough to replace it with a StackPanel as

 <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel/> <ItemsPanelTemplate> <ListBox.ItemsPanel> 
+3
source share

You may be mistaken. What I did was bind the Loaded [content] event of my DataTemplate:

 <DataTemplate DataType="{x:Type local:ProjectPersona}"> <Grid Loaded="Row_Loaded"> <!-- ... --> </Grid> </DataTemplate> 

... and then process the newly displayed string in the event handler:

 private void Row_Loaded(object sender, RoutedEventArgs e) { Grid grid = (Grid)sender; Carousel c = (Carousel)grid.FindName("carousel"); ProjectPersona project = (ProjectPersona)grid.DataContext; if (project.SelectedTime != null) c.ScrollItemIntoView(project.SelectedTime); } 

This approach initializes / checks the line when it is first displayed, so it will not execute all lines up. If you can live with this, then perhaps this is a more elegant method.

+3
source share

Andy's solution is a very good idea, but incomplete. For example, the first 5 containers are created on the panel. The list contains 300> items. I am requesting the last container with this ADD logic. Then I request the last container with index - 1, with this logis ADD! This is problem. The order of the children inside the panel is invalid.

The solution for this:

  private FrameworkElement GetContainerForIndex(int index) { if (ItemsControl == null) { return null; } var container = ItemsControl.ItemContainerGenerator.ContainerFromIndex(index -1); if (container != null && container != DependencyProperty.UnsetValue) { return container as FrameworkElement; } else { var virtualizingPanel = FindVisualChild<VirtualizingPanel>(ItemsControl); if (virtualizingPanel == null) { // do something to load the (perhaps currently unloaded panel) once } virtualizingPanel = FindVisualChild<VirtualizingPanel>(ItemsControl); IItemContainerGenerator generator = ItemsControl.ItemContainerGenerator; using (generator.StartAt(generator.GeneratorPositionFromIndex(index), GeneratorDirection.Forward)) { bool isNewlyRealized = false; container = generator.GenerateNext(out isNewlyRealized); if (isNewlyRealized) { generator.PrepareItemContainer(container); bool insert = false; int pos = 0; for (pos = virtualizingPanel.Children.Count - 1; pos >= 0; pos--) { var idx = ItemsControl.ItemContainerGenerator.IndexFromContainer(virtualizingPanel.Children[pos]); if (!insert && idx < index) { ////Add virtualizingPanel.GetType().InvokeMember("AddInternalChild", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, Type.DefaultBinder, virtualizingPanel, new object[] { container }); break; } else { insert = true; if (insert && idx < index) { break; } } } if (insert) { virtualizingPanel.GetType().InvokeMember("InsertInternalChild", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, Type.DefaultBinder, virtualizingPanel, new object[] { pos + 1, container }); } } return container as FrameworkElement; } } } 
+1
source share

For someone who wonders about this, in the case of Andy, perhaps replacing VirtualizationStackPanel with a regular StackPanel would be a better solution.

The reason that calling PrepareItemContainer in ItemContainerGenerator does not work is because the item must be in the visual tree for PrepareItemContainer to work. With VirtualizationStackPanel, the item will not be set as a visual child of the panel until VirtualizingStackPanel determines that it / is on the screen.

Another solution (the one I'm using) is to create your own VirtualizationPanel, so you can control when elements are added to the visual tree.

0
source share

In my case, I found that calling UpdateLayout() on the ItemsControl ( ListBox , ListView , etc.) launched its ItemContainerGenerator , so the generator status changed from "NotStarted" to "NotStarted", GeneratingContainers "and null no longer returned ItemContainerGenerator.ContainerFromItem and / or ItemContainerGenerator.ContainerFromIndex .

For example:

  public static bool FocusSelectedItem(this ListBox listbox) { int ix; if ((ix = listbox.SelectedIndex) < 0) return false; var icg = listbox.ItemContainerGenerator; if (icg.Status == GeneratorStatus.NotStarted) listbox.UpdateLayout(); var el = (UIElement)icg.ContainerFromIndex(ix); if (el == null) return false; listbox.ScrollIntoView(el); return el == Keyboard.Focus(el); } 
-one
source share

All Articles