-1

我无法从ListBoxin获取项目容器Backstage。说,我有以下内容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>

我填写:

public Root()
{
  ContentRendered += delegate
  {
    var list = new List<int> { 1, 2, 3 };
    foreach (var index in list)
    {
      lstColumns.Items.Add(index);
    }
  };
}

接下来,我想ListBoxItem从 的第一个条目中检索项目容器(在本例中为 - )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");
  }
}

container总是null。但!如果我打开Backstage然后隐藏它,我会看到以下消息:

container = System.Windows.Controls.ListBoxItem.

所以,我决定添加Backstage在填充之前打开的代码:

backStage.IsOpen = true;
var list = new List<int> { 1, 2, 3 };
foreach (var index in list)
{
  lstColumns.Items.Add(index);
}
backStage.IsOpen = false;

可行,但是当您几乎看不到Backstage显示和隐藏时会闪烁。这不是完美的解决方案。那么,如何获取物品容器呢?

PS 测试项目在这里

更新(解释)

我需要项目容器的原因是我需要CheckBox在填充时添加设置状态ListBox。这ListBox被设计为包含CheckBox项目的 es:

<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>

所以,当我在上面的循环中添加文本时,CheckBox就会创建。然后,我需要设置这些复选框的状态,这些复选框来自 JSON。所以,我需要这样的东西:

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>();
}

问题是container总是如此null。此外,无论我使用Loaded还是ContentRendered事件都没关系 - 在任何一种情况下container都是null.

4

1 回答 1

1

高级介绍

ContainerFromIndex返回的原因null是容器根本没有实现

返回与 ItemCollection 中给定索引处的项目相对应的元素,null如果项目未实现,则返回。

这由负责以下操作的ItemContainerGenerator控制。

  • 维护多项控件的数据视图(例如)ContainerFromElement与相应UIElement任务之间的关联。

  • UIElement代表多项目控件生成项目。

AListBoxItemsControl公开ItemsSource用于绑定或分配集合的属性的 an。

用于生成 ItemsControl 内容的集合。默认值为null.

另一种选择是简单地将项目添加到ItemsXAML 或代码中的集合中。

用于生成 ItemsControl 内容的集合。默认为空集合。[...]

访问集合对象本身的属性是只读的,而集合本身是可读写的。

Items属性是 type 的ItemCollection它也是一个 view

如果你有一个ItemsControl,比如一个ListBox有内容的,你可以使用该Items属性来访问ItemCollection一个视图。因为它是一个视图,所以您可以使用与视图相关的功能,例如排序、过滤和分组。请注意,当ItemsSource设置时,视图操作委托给ItemsSource集合上的视图。因此,ItemCollection只有在委托视图支持时,它们才支持排序、过滤和分组。

您不能同时使用两者ItemsSourceItems它们是相关的。

[...] 您使用ItemsItemsSource属性来指定用于生成ItemsControl. 设置ItemsSource属性后,Items集合将变为只读且大小固定。

两者ItemsSourceItems维护对数据项的引用或绑定数据项,这些都不是容器。ItemContainerGenerator负责创建用户界面元素或容器等,并维护数据与这些项目之间的ListBoxItem关系。这些容器不仅存在于应用程序的整个生命周期中,它们还会根据需要创建和销毁。什么时候发生?这取决于。容器被创建或实现(使用内部术语)当它们显示在 UI 中时。这就是为什么您只有在容器首次显示后才能访问它。它们实际存在多长时间取决于交互、虚拟化或容器回收等因素。我所说的交互是指任何形式的更改视口,这是您实际可以看到的列表的一部分。每当项目滚动到视图中时,当然需要实现它们。对于包含数万个项目的大型列表,提前实现所有容器或在实现后保留所有容器会影响性能并大幅增加内存消耗。这就是虚拟化发挥作用的地方。请参阅显示大型数据集以供参考。

UI 虚拟化是列表控件的一个重要方面。UI 虚拟化不应与数据虚拟化相混淆。UI 虚拟化仅将可见项存储在内存中,但在数据绑定场景中,将整个数据结构存储在内存中。相反,数据虚拟化仅将屏幕上可见的数据项存储在内存中。

默认情况下,当 ListView 和 ListBox 控件的列表项绑定到数据时,它们会启用 UI 虚拟化。

这意味着容器也被删除。此外,还有容器回收

ItemsControl使用 UI 虚拟化填充时,它会为每个滚动到视图中的项目创建一个项目容器,并为每个滚动到视图之外的项目销毁项目容器。容器回收使控件能够为不同的数据项重用现有的项容器,以便在用户滚动 ItemsControl 时不会不断地创建和销毁项容器。VirtualizationMode您可以通过将附加属性设置为 来选择启用项目回收Recycling

虚拟化和容器回收的结果是所有物品的容器一般都没有实现。您的绑定或分配物品的子集只有容器,它们可以回收或分离。这就是为什么直接引用 eg ListBoxItems 是危险的。即使禁用了虚拟化,您也可能会遇到像您这样的问题,尝试访问与数据项具有不同生命周期的用户界面元素。

从本质上讲,您的方法可以工作,但我推荐一种更稳定、更健壮且与所有上述警告兼容的不同方法。

低级视图

这里实际发生了什么?让我们以中等深度探索代码,因为我的手腕已经受伤了。

这是.NETContainerFromIndex参考源中的方法。

  • for931 行的循环ItemBlock使用.Next_itemMap
  • 当您的项目未显示但在用户界面中时,它们不会被实现。
  • 在这种情况下,Next将返回一个UnrealizedItemBlock(的导数ItemBlock)。
  • 这个项目块的属性ItemCount为零。
  • 将不满足if第 933 行中的条件。
  • 这一直持续到项目块被迭代并null在第 954 行返回

一旦ListBox显示了 the 及其项,Next迭代器将返回 a RealizedItemBlock,它的aItemCount大于零,因此将产生一个项。

那么容器是如何实现的呢?有一些方法可以生成容器。

  • DependencyObject IItemContainerGenerator.GenerateNext(),见第 230 行
  • DependencyObject IItemContainerGenerator.GenerateNext(out bool isNewlyRealized)见第 239 行

这些在不同的地方被调用,比如VirtualizingStackPanel- 用于虚拟化。

  • protected internal override void BringIndexIntoView(int index),请参见第 1576 行,这正是它所称的。当需要将具有特定索引的项目带入视图时,例如通过滚动,面板需要创建项目容器以便在用户界面中显示该项目。
  • private void MeasureChild(...),见第 8005 行。在计算显示 a 所需的空间时使用此方法,该空间根据需要ListView受其项目的数量和大小的影响。
  • ...

ListBox在其基类型的高级别的大量间接调用中ItemsControl,最终ItemContainerGenerator调用 来实现项目。

符合 MVVM 标准的解决方案

对于前面提到的所有问题,有一个简单但优越的解决方案。将您的数据和应用程序逻辑与用户界面分开。这可以使用 MVVM 设计模式来完成。有关介绍,您可以参考 Josh Smith 的Patterns - WPF Apps With The Model-View-ViewModel Design Pattern 一文。

在此解决方案中,我使用 Microsoft 的Microsoft.Toolkit.Mvvm NuGet 包。您可以在此处找到介绍和详细文档。我使用它是因为对于 WPF 中的 MVVM,您需要一些用于可观察对象和命令的样板代码,这会使初学者的示例变得臃肿。这是一个很好的库,可以开始并稍后了解这些工具如何在幕后工作的详细信息。

那么让我们开始吧。在新解决方案中安装上述 NuGet 包。接下来,创建一个表示我们的数据项的类型。它只包含两个属性,一个用于索引,它是只读的,一个用于可以更改的选中状态。绑定仅适用于属性,这就是我们使用它们而不是字段的原因。类型派生自ObservableObject实现INotifyPropertyChanged接口的类型。需要实现这个接口,以便能够通知属性值发生了变化,否则后面引入的绑定将不知道何时更新用户界面中的值。ObservableObject基本类型已经提供了一种方法,该SetProperty方法将负责为属性的支持字段设置新值并自动通知其更改。

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.
   }
}

现在我们为您的视图实现一个视图模型Root,它保存用户界面的数据。它公开了一个ObservableCollection<JsonItem>我们用来存储 JSON 数据项的属性。如果添加、删除或替换任何项目,此特殊集合会自动通知。这对于您的示例不是必需的,但我想它以后可能对您有用。您也可以替换整个集合,因为我们再次派生ObservableObject和使用SetProperty. 这GetPropertiesCommand是一个命令,它只是一个封装的动作,一个执行任务的对象。它可以绑定并在Click以后替换处理程序。该CreateItems方法只是像您的示例一样创建一个列表。这GetProperties是您迭代列表并从 JSON 设置值的方法。根据您的需要调整代码。

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.
         }
      }
   }
}

您的视图的代码隐藏Root现在已简化为基本要素,不再有数据。

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();
            }
         };
      }
   }
}

Root最后,我们为视图创建 XAML 。我已添加评论供您跟进。本质上,我们添加了新RootViewModel的 asDataContext并使用数据绑定将我们的数据项集合与ListBoxviaItemsSource属性连接起来。此外,我们使用 aDataTemplate来定义用户界面中数据的外观并将 绑定Button到命令。

<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>

现在有什么区别?数据和您的应用程序逻辑与用户界面分离。无论项目容器如何,数据始终存在于视图模型中。事实上,你的数据甚至不知道有一个容器或一个ListBox. 后台是否开放不再重要,因为您直接对数据进行操作,而不是用户界面。

更快更脏的解决方案

我不推荐这个解决方案,它只是一个除了 MVVM 之外的快速而肮脏的解决方案,在你看到如何正确地做之后可能更容易理解。它使用JsonItem以前的类型,但这次没有外部库。现在你看到INotifyPropertyChanged了引擎盖下的作用。

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));
      }
   }
}

Root视图的代码隐藏中,只需创建一个_jsonItems存储项目的字段。此字段用于稍后访问列表以更改IsChecked值。

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.
         }
      }
   }
}

最后对于Root视图没有太大的变化。我们使用 MVVM 示例中的数据模板复制样式并将其设置为ListBox. 它只是表现相同,因为您的数据不依赖于视图容器。

<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}"/>
于 2022-03-01T23:17:33.580 回答