web-dev-qa-db-fra.com

 ItemContainerGenerator.ContainerFromItem () renvoie null?

J'ai un comportement bizarre que je n'arrive pas à résoudre. Lorsque je parcours les éléments de ma propriété ListBox.ItemsSource, je n'arrive pas à obtenir le conteneur? Je m'attends à voir un ListBoxItem renvoyé, mais je n'obtiens que null.

Des idées?

Voici le morceau de code que j'utilise:

this.lstResults.ItemsSource.ForEach(t =>
    {
        ListBoxItem lbi = this.lstResults.ItemContainerGenerator.ContainerFromItem(t) as ListBoxItem;

        if (lbi != null)
        {
            this.AddToolTip(lbi);
        }
    });

ItemsSource est actuellement défini sur un dictionnaire et contient un certain nombre de KVP.

32
Sonny Boy

Enfin réglé le problème ... En ajoutant VirtualizingStackPanel.IsVirtualizing="False" dans mon XAML, tout fonctionne maintenant comme prévu.

En revanche, je ne vois pas tous les avantages de la virtualisation en termes de performances. J'ai donc modifié mon routage de charge en asynchrone et ajouté un "spinner" dans ma zone de liste pendant le chargement ...

14
Sonny Boy

J'ai trouvé quelque chose qui fonctionnait mieux pour mon cas dans cette question de StackOverflow:

Obtenir une ligne dans une grille de données

En plaçant dans les appels UpdateLayout et ScrollIntoView avant d'appeler ContainerFromItem ou ContainerFromIndex, vous indiquez la partie du DataGrid qui permet de renvoyer une valeur pour ContainerFromItem/ContainerFromIndex:

dataGrid.UpdateLayout();
dataGrid.ScrollIntoView(dataGrid.Items[index]);
var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(index);

Si vous ne voulez pas que l'emplacement actuel dans DataGrid change, ce n'est probablement pas une bonne solution pour vous, mais si cela vous convient, cela fonctionne sans avoir à désactiver la virtualisation.

44
Phred Menyhert
object viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
if (viewItem == null)
{
    list.UpdateLayout();
    viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
    Debug.Assert(viewItem != null, "list.ItemContainerGenerator.ContainerFromItem(item) is null, even after UpdateLayout");
}
8
epox

Parcourez le code avec le débogueur et voyez s’il n’y a rien de rétabli ou si la distribution as- est tout simplement fausse et la transforme donc en null (vous pouvez simplement utiliser une distribution normale pour obtenir une exception appropriée).

Un problème qui se produit fréquemment est que, lorsqu'un ItemsControl est en train de virtualiser pour la plupart des éléments, aucun conteneur n’existera à un moment donné.

De plus, je ne recommanderais pas de traiter directement avec les conteneurs d’articles mais plutôt avec des propriétés contraignantes et de s’abonner à des événements (via le ItemsControl.ItemContainerStyle).

6
H.B.

Utilisez cet abonnement:

TheListBox.ItemContainerGenerator.StatusChanged += (sender, e) =>
{
  TheListBox.Dispatcher.Invoke(() =>
  {
     var TheOne = TheListBox.ItemContainerGenerator.ContainerFromIndex(0);
     if (TheOne != null)
       // Use The One
  });
};
3
Amir

Je suis un peu en retard pour la fête, mais voici une autre solution qui est infaillible dans mon cas,

Après avoir essayé de nombreuses solutions suggérant d’ajouter IsExpanded et IsSelected à un objet sous-jacent et de les lier dans un style TreeViewItem, alors que la plupart du temps fonctionne dans certains cas, il échoue toujours ...

Remarque: mon objectif était d'écrire une vue mini/personnalisée semblable à celle de l'Explorateur. Lorsque je clique sur un dossier dans le volet de droite, il est sélectionné dans la TreeView, comme dans l'Explorateur.

private void ListViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    var item = sender as ListViewItem;
    var node = item?.Content as DirectoryNode;
    if (node == null) return;

    var nodes = (IEnumerable<DirectoryNode>)TreeView.ItemsSource;
    if (nodes == null) return;

    var queue = new Stack<Node>();
    queue.Push(node);
    var parent = node.Parent;
    while (parent != null)
    {
        queue.Push(parent);
        parent = parent.Parent;
    }

    var generator = TreeView.ItemContainerGenerator;
    while (queue.Count > 0)
    {
        var dequeue = queue.Pop();
        TreeView.UpdateLayout();
        var treeViewItem = (TreeViewItem)generator.ContainerFromItem(dequeue);
        if (queue.Count > 0) treeViewItem.IsExpanded = true;
        else treeViewItem.IsSelected = true;
        generator = treeViewItem.ItemContainerGenerator;
    }
}

Plusieurs astuces utilisées ici:

  • une pile pour agrandir chaque article de haut en bas
  • assurez-vous d'utiliser le niveau actuel générateur pour trouver l'élément (très important)
  • le fait que le générateur pour les éléments de niveau supérieur ne renvoie jamais null

Jusqu'à présent, cela fonctionne très bien,

  • pas besoin de polluer vos types avec de nouvelles propriétés  
  • pas besoin de désactiver la virtualisation du tout .
3
Aybe

Bien que la désactivation de la virtualisation à partir de XAML fonctionne, il est préférable de la désactiver à partir du fichier .cs qui utilise ContainerFromItem

 VirtualizingStackPanel.SetIsVirtualizing(listBox, false);

De cette façon, vous réduisez le couplage entre le code XAML et le code; vous éviterez ainsi que quelqu'un enfreigne le code en touchant le XAML.

2
Benoit Blanchon

VirtualizingStackPanel.IsVirtualizing = "False" Rend le contrôle flou. Voir la mise en œuvre ci-dessous. Ce qui m'aide à éviter le même problème . Configurez votre application VirtualizingStackPanel.IsVirtualizing = "True" toujours.

Voir le link pour des informations détaillées

/// <summary>
/// Recursively search for an item in this subtree.
/// </summary>
/// <param name="container">
/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
/// </param>
/// <param name="item">
/// The item to search for.
/// </param>
/// <returns>
/// The TreeViewItem that contains the specified item.
/// </returns>
private TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
    if (container != null)
    {
        if (container.DataContext == item)
        {
            return container as TreeViewItem;
        }

        // Expand the current container
        if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
        {
            container.SetValue(TreeViewItem.IsExpandedProperty, true);
        }

        // Try to generate the ItemsPresenter and the ItemsPanel.
        // by calling ApplyTemplate.  Note that in the 
        // virtualizing case even if the item is marked 
        // expanded we still need to do this step in order to 
        // regenerate the visuals because they may have been virtualized away.

        container.ApplyTemplate();
        ItemsPresenter itemsPresenter = 
            (ItemsPresenter)container.Template.FindName("ItemsHost", container);
        if (itemsPresenter != null)
        {
            itemsPresenter.ApplyTemplate();
        }
        else
        {
            // The Tree template has not named the ItemsPresenter, 
            // so walk the descendents and find the child.
            itemsPresenter = FindVisualChild<ItemsPresenter>(container);
            if (itemsPresenter == null)
            {
                container.UpdateLayout();

                itemsPresenter = FindVisualChild<ItemsPresenter>(container);
            }
        }

        Panel itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);


        // Ensure that the generator for this panel has been created.
        UIElementCollection children = itemsHostPanel.Children; 

        MyVirtualizingStackPanel virtualizingPanel = 
            itemsHostPanel as MyVirtualizingStackPanel;

        for (int i = 0, count = container.Items.Count; i < count; i++)
        {
            TreeViewItem subContainer;
            if (virtualizingPanel != null)
            {
                // Bring the item into view so 
                // that the container will be generated.
                virtualizingPanel.BringIntoView(i);

                subContainer = 
                    (TreeViewItem)container.ItemContainerGenerator.
                    ContainerFromIndex(i);
            }
            else
            {
                subContainer = 
                    (TreeViewItem)container.ItemContainerGenerator.
                    ContainerFromIndex(i);

                // Bring the item into view to maintain the 
                // same behavior as with a virtualizing panel.
                subContainer.BringIntoView();
            }

            if (subContainer != null)
            {
                // Search the next level for the object.
                TreeViewItem resultContainer = GetTreeViewItem(subContainer, item);
                if (resultContainer != null)
                {
                    return resultContainer;
                }
                else
                {
                    // The object is not under this TreeViewItem
                    // so collapse it.
                    subContainer.IsExpanded = false;
                }
            }
        }
    }

    return null;
}

Pour tous ceux qui rencontraient encore des problèmes avec cela, j'ai pu contourner ce problème en ignorant le premier événement modifié de sélection et en utilisant un fil pour répéter l'appel en gros. Voici ce que j'ai fini par faire:

private int _hackyfix = 0;
    private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        //HACKYFIX:Hacky workaround for an api issue
        //Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason.  Basically we ignore the
        //first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
        //with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need I ignore the event twice just in case but I think you can get away with ignoring only the first one.
        if (_hackyfix == 0 || _hackyfix == 1)
        {
            _hackyfix++;
            Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            OnMediaSelectionChanged(sender, e);
        });
        }
        //END OF HACKY FIX//Actual code you need to run goes here}

EDIT 10/29/2014: En réalité, vous n'avez même pas besoin du code du répartiteur de threads. Vous pouvez définir tout ce dont vous avez besoin sur null pour déclencher le premier événement modifié de sélection, puis revenir en dehors de l'événement afin que les événements futurs fonctionnent comme prévu.

        private int _hackyfix = 0;
    private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        //HACKYFIX: Daniel note:  Very hacky workaround for an api issue
        //Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason.  Basically we ignore the
        //first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
        //with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need
        if (_hackyfix == 0)
        {
            _hackyfix++;
            /*
            Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            OnMediaSelectionChanged(sender, e);
        });*/
            return;
        }
        //END OF HACKY FIX
        //Your selection_changed code here
        }
1
Daniel Caban

Il s'agit probablement d'un problème lié à la virtualisation. Les conteneurs ListBoxItem sont générés uniquement pour les éléments actuellement visibles (voir https://msdn.Microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel(v=vs .110) .aspx # Anchor_9 )

Si vous utilisez ListBox, je vous conseillerais plutôt de passer à ListView: il hérite de ListBoxet prend en charge la méthode ScrollIntoView() que vous pouvez utiliser pour contrôler la virtualisation.

targetListView.ScrollIntoView(itemVM);
DoEvents();
ListViewItem itemContainer = targetListView.ItemContainerGenerator.ContainerFromItem(itemVM) as ListViewItem;

(l'exemple ci-dessus utilise également la méthode statique DoEvents() expliquée plus en détail ici; WPF comment attendre que la mise à jour de la liaison ait lieu avant de traiter davantage de code? )

Il existe quelques autres différences mineures entre les contrôles ListBox et ListView ( Quelle est la différence entre ListBox et ListView ) - ce qui ne devrait pas essentiellement affecter votre cas d'utilisation.

0
fvojvodic