Home > OS >  Can't get Item Container from Backstage in Fluent Ribbon
Can't get Item Container from Backstage in Fluent Ribbon

Time:03-03

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.

CodePudding user response:

A High-Level Introduction

The reason that ContainerFromIndex returns null is that the container simply is not realized.

Returns the element corresponding to the item at the given index within the ItemCollection or returns null if the item is not realized.

This is controlled by the ItemContainerGenerator that is responsible for the following actions.

  • Maintains an association between the data view of a multiple-item control, such as ContainerFromElement and the corresponding UIElement tasks.

  • Generates UIElement items on behalf of a multiple-item control.

A ListBox is an ItemsControl that exposes the ItemsSource property for binding or assigning a collection.

A collection that is used to generate the content of the ItemsControl. The default is null.

Another option is to simply add items to the Items collection in XAML or code.

The collection that is used to generate the content of the ItemsControl. The default is an empty collection. [...]

The property to access the collection object itself is read-only, and the collection itself is read-write.

The Items property is of type ItemCollection, which is also a view.

If you have an ItemsControl, such as a ListBox that has content, you can use the Items property to access the ItemCollection, which is a view. Because it is a view, you can then use the view-related functionalities such as sorting, filtering, and grouping. Note that when ItemsSource is set, the view operations delegate to the view over the ItemsSource collection. Therefore, the ItemCollection supports sorting, filtering, and grouping only if the delegated view supported them.

You cannot use both ItemsSource and Items at the same time, they are related.

[...] you use either the Items or the ItemsSource property to specify the collection that should be used to generate the content of your ItemsControl. When the ItemsSource property is set, the Items collection is made read-only and fixed-size.

Both ItemsSource and Items either maintain a reference to or bind your data items, these are not the containers. The ItemContainerGenerator is responsible for creating the user interface elements or containers such as ListBoxItem and 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.

UI Virtualization is an important aspect of list controls. UI virtualization should not be confused with data virtualization. UI virtualization stores only visible items in memory but in a data-binding scenario stores the entire data structure in memory. In contrast, data virtualization stores only the data items that are visible on the screen in memory.

By default, UI virtualization is enabled for the ListView and ListBox controls when their list items are bound to data.

This implies that containers are deleted, too. Additionally, there is container recycling:

When an ItemsControl that uses UI virtualization is populated, it creates an item container for each item that scrolls into view and destroys the item container for each item that scrolls out of view. Container recycling enables the control to reuse the existing item containers for different data items, so that item containers are not constantly created and destroyed as the user scrolls the ItemsControl. You can choose to enable item recycling by setting the VirtualizationMode attached property to 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 ContainerFromIndex method in the reference source of .NET.

  • The for loop in line 931 iterates ItemBlocks using the Next property of the _itemMap.
  • When your items were not shown, yet in the user interface, they are not realized.
  • In this case, Next will return an UnrealizedItemBlock (derivative of ItemBlock).
  • This item block will have a property ItemCount of zero.
  • The if condition in line 933 will not be met.
  • This continues until the item blocks are iterated and null is returned in line 954..

Once the ListBox and its items are shown, the Next iterator will return a RealizedItemBlock which has an ItemCount of 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 a ListView, which is influenced by the number and size of its items as needed.
  • ...

Over lots of indirections from a high-level ListBox over its base type ItemsControl, ultimately, the ItemContainerGenerator is 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 ObservableObject which implements the INotifyPropertyChanged interface. 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. The ObservableObject base type already provides a SetProperty method that will take care of setting a new value to the backing field of a property and automatically notify its change.

using Microsoft.Toolkit.Mvvm.ComponentModel;

namespace RibbonBackstageFillTest
{
   public class JsonItem : ObservableObject
   {
      private bool _isChecked;

      public JsonItem(int index, bool isChecked)
      {
         Index = index;
         IsChecked = isChecked;
      }

      // ...read-only property assumed here.
      public int Index { get; }

      public bool IsChecked
      {
         get => _isChecked;
         set => SetProperty(ref _isChecked, value);
      }

      // ...other properties.
   }
}

Now we implement a view model for your Root view, which holds the data for the user interface. It exposes an ObservableCollection<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 from ObservableObject and use SetProperty. The GetPropertiesCommand is a command, which is just an encapsulated action, an object that performs a task. It can be bound and replaces the Click handler later. The CreateItems method simply creates a list like in your example. The GetProperties is the method where you iterate the list and set your values from JSON. Adapt the code to your needs.

using System.Collections.ObjectModel;
using System.Windows.Input;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;

namespace RibbonBackstageFillTest
{
   public class RootViewModel : ObservableObject
   {
      private ObservableCollection<JsonItem> _jsonItems;

      public RootViewModel()
      {
         JsonItems = CreateItems();
         GetPropertiesCommand = new RelayCommand(GetProperties);
      }

      public ObservableCollection<JsonItem> JsonItems
      {
         get => _jsonItems;
         set => SetProperty(ref _jsonItems, value);
      }

      public ICommand GetPropertiesCommand { get; }

      private ObservableCollection<JsonItem> CreateItems()
      {
         return new ObservableCollection<JsonItem>
         {
            new JsonItem(1, false),
            new JsonItem(2, true),
            new JsonItem(3, false),
            new JsonItem(4, true),
            new JsonItem(5, false)
         };
      }

      private void GetProperties()
      {
         foreach (var jsonItem in JsonItems)
         {
            jsonItem.IsChecked = // ...set your JSON values here.
         }
      }
   }
}

The code-behind of your Root view is now reduced to its essentials, no data anymore.

using Fluent;
using Fluent.Localization.Languages;
using System.Threading;
using System.Windows;

namespace RibbonBackstageFillTest
{
   public partial class Root
   {
      public Root()
      {
         InitializeComponent();
         WindowStartupLocation = WindowStartupLocation.CenterScreen;
         ContentRendered  = delegate
         {
            if (Thread.CurrentThread.CurrentUICulture.Name != "en-US")
            {
               RibbonLocalization.Current.LocalizationMap.Clear();
               RibbonLocalization.Current.Localization = new English();
            }
         };
      }
   }
}

At last, we create the XAML for the Root view. I have added comments for you to follow along. In essence, we add the new RootViewModel as DataContext and use data-binding to connect our data item collection with the ListBox via the ItemsSource property. Furthermore, we use a DataTemplate to define the appearance of the data in the user interface and bind the Button to a command.

<r:RibbonWindow x:Class="RibbonBackstageFillTest.Root"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                xmlns:r="urn:fluent-ribbon"
                xmlns:local="clr-namespace:RibbonBackstageFillTest"
                mc:Ignorable="d"
                Title="Backstage Ribbon"
                Height="450"
                Width="800">
   <r:RibbonWindow.DataContext>
      <!-- This creates an instance of the root view model and assigns it as data context. -->
      <local:RootViewModel/>
   </Window.DataContext>
   <Window.Resources>
      <Style x:Key="CheckBoxListStyle"
             TargetType="ListBox">
         <Setter Property="SelectionMode" Value="Multiple" />

         <!-- This is only used to style the containers, we do not need to change the control template -->
         <Setter Property="ItemContainerStyle">
            <Setter.Value>
               <Style TargetType="ListBoxItem">
                  <Setter Property="Margin" Value="2" />
               </Style>
            </Setter.Value>
         </Setter>

         <!-- An item template is used to define the appearance of a data item. -->
         <Setter Property="ItemTemplate">
            <Setter.Value>
               <!-- We create a data template for our custom item type. -->
               <DataTemplate DataType="local:JsonItem">
                  <!-- The binding will loosely connect the IsChecked property of CheckBox with the IsChecked property of its JsonItem. -->
                  <!-- The binding is TwoWay by default, meaning that you can change IsChecked in code or in the UI by clicking the CheckBox. -->
                  <!-- The IsChecked value will always be synchronized in the view and view model. -->
                  <CheckBox Focusable="False"
                            IsChecked="{Binding Path=IsChecked}"/>
               </DataTemplate>
            </Setter.Value>
         </Setter>
      </Style>
   </r:RibbonWindow.Resources>
   <Grid>
      <Grid.RowDefinitions>
         <RowDefinition Height="Auto" />
         <RowDefinition />
      </Grid.RowDefinitions>

      <r:Ribbon Grid.Row="0">

         <!-- Backstage -->
         <r:Ribbon.Menu>
            <r:Backstage>
               <r:BackstageTabControl>
                  <r:BackstageTabItem Header="Columns">
                     <Grid>
                        <!-- No need for a name anymore, we do not need to access controls. -->
                        <!-- The binding loosely connects the JsonItems collection with the ListBox. -->
                        <ListBox ItemsSource="{Binding JsonItems}"
                                 Style="{StaticResource CheckBoxListStyle}"/>
                     </Grid>
                  </r:BackstageTabItem>
               </r:BackstageTabControl>
            </r:Backstage>
         </r:Ribbon.Menu>

         <!-- Tabs -->
         <r:RibbonTabItem Header="Home">
            <r:RibbonGroupBox Header="ID">
               <!-- Instead of a Click event handler, we bind a command in the view model. -->
               <r:Button Size="Large"
                         LargeIcon="pack://application:,,,/RibbonBackstageFillTest;component/img/PropertySheet.png"
                         Command="{Binding GetPropertiesCommand}"
                         Header="Properties"/>
            </r:RibbonGroupBox>
         </r:RibbonTabItem>
      </r:Ribbon>

   </Grid>
</r:RibbonWindow>

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 JsonItem type from before, but this time without an external library. Now you see what INotifyPropertyChanged does under the hood.

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace RibbonBackstageFillTest
{
   public class JsonItem : INotifyPropertyChanged
   {
      private bool _isChecked;

      public JsonItem(int index, bool isChecked)
      {
         Index = index;
         IsChecked = isChecked;
      }

      // ...read-only property assumed here.
      public int Index { get; }

      public bool IsChecked
      {
         get => _isChecked;
         set
         {
            if (_isChecked == value)
               return;

            _isChecked = value;
            OnPropertyChanged();
         }
      }

      // ...other properties.

      public event PropertyChangedEventHandler PropertyChanged;

      protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
   }
}

In your code-behind of the Root view, just create a field _jsonItems that stores the items. This field is used to access the list later in order to change the IsChecked values.

using Fluent;
using Fluent.Localization.Languages;
using System.Collections.Generic;
using System.Threading;
using System.Windows;

namespace RibbonBackstageFillTest
{
   public partial class Root
   {
      private List<JsonItem> _jsonItems;

      public Root()
      {
         InitializeComponent();
         WindowStartupLocation = WindowStartupLocation.CenterScreen;
         ContentRendered  = delegate
         {
            if (Thread.CurrentThread.CurrentUICulture.Name != "en-US")
            {
               RibbonLocalization.Current.LocalizationMap.Clear();
               RibbonLocalization.Current.Localization = new English();
            }
         };

         _jsonItems = new List<JsonItem>
         {
            new JsonItem(1, false),
            new JsonItem(2, true),
            new JsonItem(3, false),
            new JsonItem(4, true),
            new JsonItem(5, false)
         };
         lstColumns.ItemsSource = _jsonItems;
      }

      private void OnGetProperties(object sender, RoutedEventArgs e)
      {
         foreach (var jsonItem in _jsonItems)
         {
            jsonItem.IsChecked = // ...set your JSON value.
         }
      }
   }
}

At last for the Root view not much changes. We copy the style with the data template from the MVVM sample and set it to the ListBox. It will just behave the same, as your data is not dependent on view containers.

<r:RibbonWindow.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" />
            </Style>
         </Setter.Value>
      </Setter>

      <Setter Property="ItemTemplate">
         <Setter.Value>
            <DataTemplate DataType="local:JsonItem">
               <CheckBox Focusable="False"
                         IsChecked="{Binding Path=IsChecked}"/>
            </DataTemplate>
         </Setter.Value>
      </Setter>
   </Style>
</r:RibbonWindow.Resources>
<ListBox x:Name="lstColumns"
         Style="{StaticResource CheckBoxListStyle}"/>
  • Related