I cannot get item container from the ListBox in Backstage. Say, I have the following Backstage:
<!-- Backstage -->
<r:Ribbon.Menu>
<r:Backstage x:Name="backStage">
<r:BackstageTabControl>
<r:BackstageTabItem Header="Columns">
<Grid>
<ListBox Grid.Row="1" Grid.Column="0" x:Name="lstColumns"/>
</Grid>
</r:BackstageTabItem>
</r:BackstageTabControl>
</r:Backstage>
</r:Ribbon.Menu>
I fill it up:
public Root()
{
ContentRendered += delegate
{
var list = new List<int> { 1, 2, 3 };
foreach (var index in list)
{
lstColumns.Items.Add(index);
}
};
}
Next, I want to retrieve the item container (in this case - ListBoxItem) from the first entry of ListBox:
private void OnGetProperties(object sender, RoutedEventArgs e)
{
// Get first item container
var container = lstColumns.ItemContainerGenerator.ContainerFromIndex(0);
if (container is not null)
{
MessageBox.Show($"container = {container.GetType().FullName}");
}
else
{
MessageBox.Show("container is null");
}
}
But container is always null. But! If I open Backstage and then hide it, I see the message:
container = System.Windows.Controls.ListBoxItem.
So, I decided to add code which opens Backstage before filling it up:
backStage.IsOpen = true;
var list = new List<int> { 1, 2, 3 };
foreach (var index in list)
{
lstColumns.Items.Add(index);
}
backStage.IsOpen = false;
This works, but there's a flickering when you can barely see that Backstage is shown and hidden. This is not the perfect solution. So, how to get the item container?
P.S. Test project is here.
UPDATE (EXPLANATION)
The reason I need the item container is that I need to add set CheckBox state upon filling ListBox. This ListBoxis styled to contain CheckBoxes for items:
<Window.Resources>
<Style x:Key="CheckBoxListStyle" TargetType="ListBox">
<Setter Property="SelectionMode" Value="Multiple"/>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="2"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<CheckBox Focusable="False"
IsChecked="{Binding Path=IsSelected,
Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}">
<ContentPresenter />
</CheckBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
So, when I add text in the loop above, the CheckBoxgets created. I, then, need to set the states of those checkboxes, which come from JSON. So, I need something like this:
var list = new List<int> { 1, 2, 3 };
var json = JsonNode.Parse("""
{
"checked": true
}
""");
foreach (var index in list)
{
CheckBox checkBox = null;
var pos = lstColumns.Items.Add(index);
var container = lstColumns.ItemContainerGenerator.ContainerFromIndex(pos);
// Reach checkbox
// ...
// checkBox = ...
// ...
checkBox.IsChecked = json["checked"].GetValue<bool>();
}
And the problem is that container is always null.
Also, it doesn't matter whether I use Loaded or ContentRendered event - in either case container is null.
A High-Level Introduction
The reason that
ContainerFromIndexreturnsnullis that the container simply is not realized.This is controlled by the ItemContainerGenerator that is responsible for the following actions.
A
ListBoxis anItemsControlthat exposes theItemsSourceproperty for binding or assigning a collection.Another option is to simply add items to the
Itemscollection in XAML or code.The
Itemsproperty is of typeItemCollection, which is also a view.You cannot use both
ItemsSourceandItemsat the same time, they are related.Both
ItemsSourceandItemseither maintain a reference to or bind your data items, these are not the containers. TheItemContainerGeneratoris responsible for creating the user interface elements or containers such asListBoxItemand maintaining the relationship between the data and these items. These containers do not just exist throughout the lifecycle of your application, they get created and destroyed as needed. When does that happen? It depends. Containers are created or realized (using the internal terminology) when they are shown in the UI. That is why you only gain access to a container after it was first shown. How long they actually exist depends on factors like interaction, virtualization or container recycling. By interaction I mean any form of changing the viewport, which is the part of the list that you can actually see. Whenever items are scrolled into view, they need to be realized of course. For large lists with tens of thousands of items, realizing all containers in advance or keeping all containers once they are realized would hit performace and increase memory consumption drastically. That is where virtualization comes into play. See Displaying large data sets for reference.This implies that containers are deleted, too. Additionally, there is container recycling:
The consequence of virtualization and container recycling is that containers for all items are not realized in general. There are only containers for a subset of your bound or assigned items and they may be recycled or detached. That is why it is dangerous to directly reference e.g.
ListBoxItems. Even if virtualization is disabled, you can run into problems like yours, trying to access user interface elements with a different lifetime than your data items.In essence, your approach can work, but I recommend a different approach that is much more stable and robust and compatible with all of the aforementioned caveats.
A Low-Level View
What is actually happening here? Let us explore the code in medium depth, as my wrists already hurt.
Here is the
ContainerFromIndexmethod in the reference source of .NET.forloop in line 931 iteratesItemBlocks using theNextproperty of the_itemMap.Nextwill return anUnrealizedItemBlock(derivative ofItemBlock).ItemCountof zero.ifcondition in line 933 will not be met.nullis returned in line 954..Once the
ListBoxand its items are shown, theNextiterator will return aRealizedItemBlockwhich has anItemCountof greater than zero and will therefore yield an item.How are the containers realized then? There are methods to generate containers.
DependencyObject IItemContainerGenerator.GenerateNext(), see line 230.DependencyObject IItemContainerGenerator.GenerateNext(out bool isNewlyRealized), see line 239.These are called in various places, like
VirtualizingStackPanel- for virtualization.protected internal override void BringIndexIntoView(int index), see line 1576, which does exactly what it is called. When an item with a certain index needs to be brought into view, e.g. through scrolling, the panel needs to create the item container in order to show the item in the user interface.private void MeasureChild(...), see line 8005. This method is used when calculating the space needed to display aListView, which is influenced by the number and size of its items as needed.Over lots of indirections from a high-level
ListBoxover its base typeItemsControl, ultimately, theItemContainerGeneratoris called to realize items.An MVVM Compliant Solution
For all the previously stated issues, there is a simple, yet superior solution. Separate your data and application logic from the user interface. This can be done using the MVVM design pattern. For an introduction, you can refer to the Patterns - WPF Apps With The Model-View-ViewModel Design Pattern article by Josh Smith.
In this solution I use the Microsoft.Toolkit.Mvvm NuGet package from Microsoft. You can find an introduction and a detailed documentation here. I use it because for MVVM in WPF you need some boilerplate code for observable objects and commands that would bloat the example for a beginner. It is a good library to start and later learn the details of how the tools work behind the scenes.
So let us get started. Install the aforementioned NuGet package in a new solution. Next, create a type that represents our data item. It only contains two properties, one for the index, which is read-only and one for the checked state that can be changed. Bindings only work with properties, that is why we use them instead of e.g. fields. The type derives from
ObservableObjectwhich implements theINotifyPropertyChangedinterface. This interface needs to be implemented to be able to notify that property values changed, otherwise the bindings that are introduced later will not know when to update the value in the user interface. TheObservableObjectbase type already provides aSetPropertymethod that will take care of setting a new value to the backing field of a property and automatically notify its change.Now we implement a view model for your
Rootview, which holds the data for the user interface. It exposes anObservableCollection<JsonItem>property that we use to store the JSON data items. This special collection automatically notifies if any items were added, removed or replaced. This is not necessary for your example, but you I guess it could be useful for you later. You can also replace the whole collection, as we again derived fromObservableObjectand useSetProperty. TheGetPropertiesCommandis a command, which is just an encapsulated action, an object that performs a task. It can be bound and replaces theClickhandler later. TheCreateItemsmethod simply creates a list like in your example. TheGetPropertiesis the method where you iterate the list and set your values from JSON. Adapt the code to your needs.The code-behind of your
Rootview is now reduced to its essentials, no data anymore.At last, we create the XAML for the
Rootview. I have added comments for you to follow along. In essence, we add the newRootViewModelasDataContextand use data-binding to connect our data item collection with theListBoxvia theItemsSourceproperty. Furthermore, we use aDataTemplateto define the appearance of the data in the user interface and bind theButtonto a command.Now what is the difference? The data and your application logic is separated from the user interface. The data is always there in the view model, regardless of an item container. In fact, your data does not even know that there is a container or a
ListBox. Whether the backstage is open or not, does not matter anymore, as you directly act on your data, not the user interface.A Quicker And Dirtier Solution
I do not recommend this solution, it is just a quick and dirty solution apart from MVVM that might be easier to follow for you after you saw how to do it right. It uses the
JsonItemtype from before, but this time without an external library. Now you see whatINotifyPropertyChangeddoes under the hood.In your code-behind of the
Rootview, just create a field_jsonItemsthat stores the items. This field is used to access the list later in order to change theIsCheckedvalues.At last for the
Rootview not much changes. We copy the style with the data template from the MVVM sample and set it to theListBox. It will just behave the same, as your data is not dependent on view containers.