web-dev-qa-db-fra.com

Faites défiler WPF ListBox vers le SelectedItem défini dans le code dans un modèle de vue

J'ai une vue XAML avec une zone de liste:

<control:ListBoxScroll ItemSource="{Binding Path=FooCollection}"
                       SelectedItem="{Binding SelectedFoo, Mode=TwoWay}"
                       ScrollSelectedItem="{Binding SelectedFoo}">
    <!-- data templates, etc. -->
</control:ListBoxScroll>

L'élément sélectionné est lié à une propriété à mon avis. Lorsque l'utilisateur sélectionne un élément dans la zone de liste, ma propriété SelectedFoo dans le modèle de vue est mise à jour. Lorsque je définis la propriété SelectedFoo dans mon modèle de vue, l'élément correct est sélectionné dans la zone de liste.

Le problème est que si le SelectedFoo qui est défini dans le code n'est pas actuellement visible, je dois en outre appeler ScrollIntoView dans la zone de liste. Puisque ma ListBox est à l'intérieur d'une vue et que ma logique est à l'intérieur de mon modèle de vue ... Je n'ai pas trouvé de moyen pratique de le faire. J'ai donc étendu ListBoxScroll:

class ListBoxScroll : ListBox
{
    public static readonly DependencyProperty ScrollSelectedItemProperty = DependencyProperty.Register(
        "ScrollSelectedItem",
        typeof(object),
        typeof(ListBoxScroll),
        new FrameworkPropertyMetadata(
            null,
            FrameworkPropertyMetadataOptions.AffectsRender, 
            new PropertyChangedCallback(onScrollSelectedChanged)));
    public object ScrollSelectedItem
    {
        get { return (object)GetValue(ScrollSelectedItemProperty); }
        set { SetValue(ScrollSelectedItemProperty, value); }
    }

    private static void onScrollSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var listbox = d as ListBoxScroll;
        listbox.ScrollIntoView(e.NewValue);
    }
}

Il expose essentiellement une nouvelle propriété de dépendance ScrollSelectedItem que je lie à la propriété SelectedFoo sur mon modèle de vue. Je me connecte ensuite à la propriété de rappel modifié de la propriété dépendante et je fais défiler l'élément nouvellement sélectionné dans la vue.

Quelqu'un d'autre connaît-il un moyen plus facile d'appeler des fonctions sur les contrôles utilisateur sur une vue XAML qui est soutenue par un modèle de vue? C'est un peu un tour pour:

  1. créer une propriété dépendante
  2. ajouter un rappel à la propriété rappel changé
  3. gérer l'appel de fonction à l'intérieur du rappel statique

Ce serait bien de mettre la logique dans le ScrollSelectedItem { set { mais le framework de dépendance semble se faufiler et parvient à fonctionner sans l'appeler.

35
James Fassett

Après avoir examiné les réponses, un thème commun est apparu: des classes externes écoutant l'événement SelectionChanged de la ListBox. Cela m'a fait réaliser que l'approche de la propriété dépendante était exagérée et je pouvais simplement écouter la sous-classe:

class ListBoxScroll : ListBox
{
    public ListBoxScroll() : base()
    {
        SelectionChanged += new SelectionChangedEventHandler(ListBoxScroll_SelectionChanged);
    }

    void ListBoxScroll_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ScrollIntoView(SelectedItem);
    }
}

Je pense que c'est la solution la plus simple qui fait ce que je veux.

Une mention honorable va à adcool2007 pour avoir évoqué les comportements. Voici quelques articles pour les personnes intéressées:

http://blogs.msdn.com/b/johngossman/archive/2008/05/07/the-attached-behavior-pattern.aspx
http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx

Je pense que pour les comportements génériques qui seront ajoutés à plusieurs contrôles utilisateur différents (par exemple, les comportements de clic, les comportements de glissement, les comportements d'animation, etc.), les comportements attachés ont beaucoup de sens. La raison pour laquelle je ne veux pas les utiliser dans ce cas particulier est que l'implémentation du comportement (appeler ScrollIntoView) n'est pas une action générique qui peut arriver à n'importe quel contrôle autre qu'un ListBox.

34
James Fassett

Avez-vous essayé d'utiliser Behavior ... Voici un ScrollInViewBehavior. Je l'ai utilisé pour ListView et DataGrid ..... Je pense que cela devrait fonctionner pour ListBox ......

Vous devez ajouter une référence à System.Windows.Interactivity utiliser Behavior<T> class

Comportement

public class ScrollIntoViewForListBox : Behavior<ListBox>
{
    /// <summary>
    ///  When Beahvior is attached
    /// </summary>
    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
    }

    /// <summary>
    /// On Selection Changed
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void AssociatedObject_SelectionChanged(object sender,
                                           SelectionChangedEventArgs e)
    {
        if (sender is ListBox)
        {
            ListBox listBox = (sender as ListBox);
            if (listBox .SelectedItem != null)
            {
                listBox.Dispatcher.BeginInvoke(
                    (Action) (() =>
                                  {
                                      listBox.UpdateLayout();
                                      if (listBox.SelectedItem !=
                                          null)
                                          listBox.ScrollIntoView(
                                              listBox.SelectedItem);
                                  }));
            }
        }
    }
    /// <summary>
    /// When behavior is detached
    /// </summary>
    protected override void OnDetaching()
    {
        base.OnDetaching();
        this.AssociatedObject.SelectionChanged -=
            AssociatedObject_SelectionChanged;

    }
}

Usage

Ajoutez un alias à XAML en tant que xmlns:i="clr-namespace:System.Windows.Interactivity;Assembly=System.Windows.Interactivity"

puis dans votre Control

        <ListBox ItemsSource="{Binding Path=MyList}"
                  SelectedItem="{Binding Path=MyItem,
                                         Mode=TwoWay}"
                  SelectionMode="Single">
            <i:Interaction.Behaviors>
                <Behaviors:ScrollIntoViewForListBox />
            </i:Interaction.Behaviors>
        </ListBox>

Désormais, chaque fois que la propriété "MyItem" est définie dans ViewModel, la liste défile lorsque les modifications sont à nouveau sélectionnées.

50
Ankesh

Étant donné qu'il s'agit strictement d'un problème d'affichage, il n'y a aucune raison pour laquelle vous ne pouvez pas avoir de gestionnaire d'événements dans le code derrière votre affichage à cet effet. Écouter ListBox.SelectionChanged et utilisez-le pour faire défiler le nouvel élément sélectionné.

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ((ListBox)sender).ScrollIntoView(e.AddedItems[0]);
}

Vous n'avez pas non plus besoin d'un ListBox dérivé pour ce faire. Utilisez simplement un contrôle standard et lorsque le ListBox.SelectedItem les changements de valeur (comme décrit dans votre question d'origine), le gestionnaire ci-dessus sera exécuté et l'élément défilera dans la vue.

    <ListBox
        ItemsSource="{Binding Path=FooCollection}"
        SelectedItem="{Binding Path=SelectedFoo}"
        SelectionChanged="ListBox_SelectionChanged"
        />

Une autre approche serait d'écrire une propriété jointe qui écoute ICollectionView.CurrentChanged puis appelle ListBox.ScrollIntoView pour le nouvel élément actuel. Il s'agit d'une approche plus "réutilisable" si vous avez besoin de cette fonctionnalité pour plusieurs zones de liste. Vous pouvez trouver un bon exemple ici pour vous aider à démarrer: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/

17
sellmeadog

Je sais que c'est une vieille question, mais ma récente recherche du même problème m'a amené à cela. Je voulais utiliser l'approche comportementale, mais je ne voulais pas d'une dépendance sur le SDK Blend juste pour me donner Behavior<T> voici donc ma solution sans elle:

public static class ListBoxBehavior
{
    public static bool GetScrollSelectedIntoView(ListBox listBox)
    {
        return (bool)listBox.GetValue(ScrollSelectedIntoViewProperty);
    }

    public static void SetScrollSelectedIntoView(ListBox listBox, bool value)
    {
        listBox.SetValue(ScrollSelectedIntoViewProperty, value);
    }

    public static readonly DependencyProperty ScrollSelectedIntoViewProperty =
        DependencyProperty.RegisterAttached("ScrollSelectedIntoView", typeof (bool), typeof (ListBoxBehavior),
                                            new UIPropertyMetadata(false, OnScrollSelectedIntoViewChanged));

    private static void OnScrollSelectedIntoViewChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var selector = d as Selector;
        if (selector == null) return;

        if (e.NewValue is bool == false)
            return;

        if ((bool) e.NewValue)
        {
            selector.AddHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler));
        }
        else
        {
            selector.RemoveHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler));
        }
    }

    private static void ListBoxSelectionChangedHandler(object sender, RoutedEventArgs e)
    {
        if (!(sender is ListBox)) return;

        var listBox = (sender as ListBox);
        if (listBox.SelectedItem != null)
        {
            listBox.Dispatcher.BeginInvoke(
                (Action)(() =>
                    {
                        listBox.UpdateLayout();
                        if (listBox.SelectedItem !=null)
                            listBox.ScrollIntoView(listBox.SelectedItem);
                    }));
        }
    }
}

puis l'utilisation est juste

<ListBox ItemsSource="{Binding Path=MyList}"
         SelectedItem="{Binding Path=MyItem, Mode=TwoWay}"
         SelectionMode="Single" 
         behaviors:ListBoxBehavior.ScrollSelectedIntoView="True">
11
Dutts

Essaye ça:

private void lstBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    lstBox.ScrollIntoView(lstBox.SelectedItem);
}
7
user3511483

J'utilise cette solution claire et facile (à mon avis)

listView.SelectionChanged += (s, e) => 
    listView.ScrollIntoView(listView.SelectedItem);

listView est le nom du contrôle ListView dans xaml, SelectedItem est affecté par mon MVVM et le code est inséré dans le constructeur dans le fichier xaml.cs.

6
honzakuzel1989

Après avoir lié diverses méthodes, j'ai trouvé que ce qui suit était le plus simple et le meilleur

lstbox.Items.MoveCurrentToLast();
lstbox.ScrollIntoView(lstbox.Items.CurrentItem);
3
Alex

J'ai pris la réponse d'Ankesh et l'ai rendue indépendante du SDK de mélange. L'inconvénient de ma solution est qu'elle s'appliquera à toutes les zones de liste de votre application. Mais l'avantage n'est pas nécessaire.

Lorsque votre application s'initialise ...

    internal static void RegisterFrameworkExtensionEvents()
    {
        EventManager.RegisterClassHandler(typeof(ListBox), ListBox.SelectionChangedEvent, new RoutedEventHandler(ScrollToSelectedItem));
    }

    //avoid "async void" unless used in event handlers (or logical equivalent)
    private static async void ScrollToSelectedItem(object sender, RoutedEventArgs e)
    {
        if (sender is ListBox)
        {
            var lb = sender as ListBox;
            if (lb.SelectedItem != null)
            {
                await lb.Dispatcher.BeginInvoke((Action)delegate
                {
                    lb.UpdateLayout();
                    if (lb.SelectedItem != null)
                        lb.ScrollIntoView(lb.SelectedItem);
                });
            }
        }
    }

Cela fait défiler toutes vos zones de liste jusqu'à celles sélectionnées (ce que j'aime comme comportement par défaut).

1
C. Tewalt