WPFにItemsControlのアイテムを作成させる
-
08-07-2019 - |
質問
ListBox
のアイテムがUIに正しく表示されていることを確認したい。これを行う1つの方法は、ビジュアルツリー内の ListBox
のすべての子を調べてテキストを取得し、それをテキストが期待するものと比較することだと考えました。
このアプローチの問題は、内部で ListBox
が VirtualizingStackPanel
を使用してアイテムを表示するため、表示されているアイテムのみが作成されることです。最終的に ItemContainerGenerator
クラスに出会いました。これは、WPFが指定されたアイテムのビジュアルツリーにコントロールを作成するように強制するように見えます。残念ながら、それは私にとって奇妙な副作用を引き起こしています。以下は、 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);
}
}
(必要に応じて GetItemText()
のコードを提供できますが、 TextBlock
が見つかるまでビジュアルツリーを走査するだけです。アイテムにテキストを含める他の方法ですが、アイテム生成が適切に機能するようになったら修正します。)
私のアプリでは、 ItemsListBox
には20個のアイテムが含まれており、最初の12個のアイテムが最初に表示されています。最初の14項目のテキストは正しいです(おそらくそれらのコントロールが既に生成されているため)。ただし、アイテム15〜20については、テキストがまったく表示されません。さらに、 ItemsListBox
の一番下までスクロールすると、アイテム15〜20のテキストも空白になります。だから、WPFのコントロールを生成するための通常のメカニズムを何らかの方法で妨害しているようです。
間違っているのは何ですか? ItemsControl
内のアイテムをビジュアルツリーに強制的に追加する別の方法またはより良い方法はありますか?
更新:修正方法はわかりませんが、これが発生している理由を見つけたと思います。 PrepareItemContainer()
の呼び出しは、アイテムを表示するために必要なコントロールを生成し、コンテナを正しい場所のビジュアルツリーに追加するという私の仮定です。これらのことのいずれも行っていないことがわかります。コンテナは、スクロールして表示するまで ItemsControl
に追加されず、その時点でコンテナ自体(つまり、 ListBoxItem
)のみが作成され、その子は作成されません。作成されます(ここにはいくつかのコントロールが追加されている必要があり、そのうちの1つはアイテムのテキストを表示する TextBlock
である必要があります)。
PrepareItemContainer()
に渡したコントロールのビジュアルツリーを走査すると、結果は同じです。どちらの場合も、 ListBoxItem
のみが作成され、その子は作成されません。
ListBoxItem
をビジュアルツリーに追加する良い方法が見つかりませんでした。ビジュアルツリーで VirtualizingStackPanel
を見つけましたが、その Children.Add()
を呼び出すと、 InvalidOperationException
が発生します(アイテムを< ItemsControl
の項目を生成するため、code> ItemPanel 。ちょうどテストとして、リフレクションを使用して AddVisualChild()
を呼び出してみました(保護されているため)が、それも機能しませんでした。
解決 3
これを行う方法を考え出したと思います。問題は、生成されたアイテムがビジュアルツリーに追加されなかったことです。いくつかの検索の後、私が思いつく最善の方法は、 ListBox
の VirtualizingStackPanel
のいくつかの保護されたメソッドを呼び出すことです。これは理想的なものではありませんが、テストのためだけのものなので、一緒に生きなければならないと思います。
これは私のために働いたものです:
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);
}
}
他のヒント
ListBoxがVirtualizingStackPanelを使用している場合は、すぐに見ます。たぶん、StackPanelのようにそれを置き換えるだけで十分でしょう。
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
<ItemsPanelTemplate>
<ListBox.ItemsPanel>
これについて間違った方法で行っている可能性があります。私がやったのは、[の内容] DataTemplateのLoadedイベントをフックすることです:
<DataTemplate DataType="{x:Type local:ProjectPersona}">
<Grid Loaded="Row_Loaded">
<!-- ... -->
</Grid>
</DataTemplate>
...そして、イベントハンドラーで新しく表示された行を処理します:
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);
}
この方法では、行が最初に表示されるときに行の初期化/チェックが行われるため、すべての行が事前に実行されるわけではありません。それと一緒に暮らすことができるなら、おそらくこれはもっとエレガントな方法でしょう。
Andyのソリューションは非常に良いアイデアですが、不完全です。たとえば、最初の5つのコンテナが作成され、パネルに表示されます。リストは300以上ありません&gt;アイテム。このロジックADDを使用して、最後のコンテナーを要求します。次に、最後のインデックス-1つのコンテナをリクエストします。このロジスティクスはADD!それが問題です。パネル内の子の順序は無効です。
これに対する解決策:
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;
}
}
}
これについて疑問に思う他の人にとって、Andyの場合、おそらくVirtualizingStackPanelを通常のStackPanelと交換することが、ここでの最良の解決策でしょう。
ItemContainerGeneratorでPrepareItemContainerを呼び出すことが機能しないのは、PrepareItemContainerが機能するためにはアイテムがビジュアルツリーに存在する必要があるためです。 VirtualizingStackPanelを使用すると、VirtualizingStackPanelが画面上にあるかどうかを判断するまで、項目はパネルの視覚的な子として設定されません。
別のソリューション(私が使用するソリューション)は、独自のVirtualizingPanelを作成することです。これにより、ビジュアルツリーにアイテムを追加するタイミングを制御できます。
私の場合、 ItemsControl
( ListBox
、 ListView
、など)、 ItemContainerGenerator
を起動し、ジェネレータのステータスが&quot; NotStarted&quot;から変更された&quot; GeneratingContainers&quot;および null
コンテナは、 ItemContainerGenerator.ContainerFromItem
および/または ItemContainerGenerator.ContainerFromIndex
によって返されなくなりました。
例:
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);
}