web-dev-qa-db-fra.com

Comment prendre en charge la liaison ListBox SelectedItems avec MVVM dans une application navigable

Je crée une application WPF qui est navigable via des boutons et des commandes personnalisées "Suivant" et "Précédent" (c’est-à-dire qui n’utilise pas une NavigationWindow). Sur un écran, j'ai une ListBox qui doit supporter plusieurs sélections (en utilisant le mode Extended). J'ai un modèle de vue pour cet écran et stocke les éléments sélectionnés en tant que propriété, car ils doivent être conservés.

Cependant, je suis conscient que la propriété SelectedItems d'un ListBox est en lecture seule. J'ai essayé de contourner le problème en utilisant cette solution ici , mais je n'ai pas été en mesure de l'adopter dans ma mise en œuvre. J'ai constaté que je ne pouvais pas différencier le moment où un ou plusieurs éléments sont désélectionnés et celui où je navigue entre les écrans (NotifyCollectionChangedAction.Remove est déclenché dans les deux cas, puisque techniquement tous les éléments sélectionnés sont désélectionnés lors de la navigation en dehors de l'écran). Mes commandes de navigation sont situées dans un modèle de vue distinct, qui gère les modèles de vue pour chaque écran. Par conséquent, je ne peux mettre aucune implémentation liée au modèle de vue contenant la variable ListBox.

J'ai trouvé plusieurs autres solutions moins élégantes, mais aucune d'entre elles ne semble imposer une liaison bidirectionnelle entre le modèle de vue et la vue.

Toute aide serait grandement appréciée. Je peux fournir une partie de mon code source si cela peut aider à comprendre mon problème.

30
caseklim

Essayez de créer une propriété IsSelected sur chacun de vos éléments de données et de lier ListBoxItem.IsSelected à cette propriété.

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
46
Rachel

Les solutions de Rachel fonctionnent très bien! Mais il y a un problème que j'ai rencontré - si vous remplacez le style de ListBoxItem, vous perdez le style d'origine qui lui est appliqué (dans mon cas, responsable de la mise en surbrillance de l'élément sélectionné, etc.). Vous pouvez éviter cela en héritant du style original:

<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>

Réglage de la note BasedOn (voir cette réponse ) .

17
Mifeet

Je ne pouvais pas trouver la solution de Rachel qui fonctionnait comme je le voulais, mais j'ai trouvé la réponse de Sandesh consistant à créer une propriété personnalisée dependency fonctionnait parfaitement pour moi. Il me suffisait d'écrire un code similaire pour un ListBox:

public class ListBoxCustom : ListBox
{
    public ListBoxCustom()
    {
        SelectionChanged += ListBoxCustom_SelectionChanged;
    }

    void ListBoxCustom_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        SelectedItemsList = SelectedItems;
    }

    public IList SelectedItemsList
    {
        get { return (IList)GetValue(SelectedItemsListProperty); }
        set { SetValue(SelectedItemsListProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemsListProperty =
       DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));

}

Dans mon modèle de vue, je viens de référencer cette propriété pour obtenir ma liste sélectionnée.

8
Ben

J'ai continué à chercher une solution facile mais sans succès.

La solution que Rachel a est bonne si vous avez déjà la propriété Selected sur l’objet dans votre ItemsSource. Si vous ne le faites pas, vous devez créer un modèle pour ce modèle d'entreprise.

J'ai emprunté un itinéraire différent. Un rapide, mais pas parfait.

Sur votre ListBox, créez un événement pour SelectionChanged.

<ListBox ItemsSource="{Binding SomeItemsSource}"
         SelectionMode="Multiple"
         SelectionChanged="lstBox_OnSelectionChanged" />

Maintenant, implémentez l'événement sur le code derrière votre page XAML.

private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var listSelectedItems = ((ListBox) sender).SelectedItems;
    ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast<ObjectType>().ToList();
}

Tada. Terminé.

Cela a été fait avec l'aide de en convertissant SelectedItemCollection en une liste .

2
AzzamAziz

Non satisfait des réponses données, j'essayais d'en trouver une par moi-même… .. Eh bien, cela ressemble plus à un bidouillage qu'à une solution, mais pour moi, cela fonctionne bien. Cette solution utilise MultiBindings d’une manière particulière. D'abord, cela peut ressembler à une tonne de code, mais vous pouvez le réutiliser avec très peu d'effort.

J'ai d'abord implémenté un 'IMultiValueConverter'

public class SelectedItemsMerger : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        SelectedItemsContainer sic = values[1] as SelectedItemsContainer;

        if (sic != null)
            sic.SelectedItems = values[0];

        return values[0];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return new[] { value };
    }
}

Et un conteneur/wrapper SelectedItems:

public class SelectedItemsContainer
{
    /// Nothing special here...
    public object SelectedItems { get; set; }
}

Maintenant, nous créons la liaison pour notre ListBox.SelectedItem (Singular). Remarque: vous devez créer une ressource statique pour le "convertisseur". Cela peut être effectué une fois par application et être réutilisé pour tous les ListBox ayant besoin du convertisseur.

<ListBox.SelectedItem>
 <MultiBinding Converter="{StaticResource SelectedItemsMerger}">
  <Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" Path="SelectedItems"/>
  <Binding Path="SelectionContainer"/>
 </MultiBinding>
</ListBox.SelectedItem>

Dans le ViewModel, j'ai créé le conteneur auquel je peux me connecter. Il est important de l'initialiser avec new () afin de le remplir avec les valeurs.

    SelectedItemsContainer selectionContainer = new SelectedItemsContainer();
    public SelectedItemsContainer SelectionContainer
    {
        get { return this.selectionContainer; }
        set
        {
            if (this.selectionContainer != value)
            {
                this.selectionContainer = value;
                this.OnPropertyChanged("SelectionContainer");
            }
        }
    }

Et c'est tout. Peut-être que quelqu'un voit des améliorations? Qu'en pensez-vous?

0
Fresch

C’était un problème majeur pour moi. Certaines des réponses que j’avais vues étaient soit trop rudimentaires, soit obligées de réinitialiser la valeur de la propriété SelectedItems en interrompant tout code associé à l’événement OnCollectionChanged de propriétés. Mais j’ai réussi à obtenir une solution viable en modifiant directement la collection et, en prime, elle prend même en charge SelectedValuePath pour les collections d’objets.

public class MultipleSelectionListBox : ListBox
{
    internal bool processSelectionChanges = false;

    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(object), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(ICollection<object>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public dynamic BindableSelectedItems
    {
        get => GetValue(BindableSelectedItemsProperty);
        set => SetValue(BindableSelectedItemsProperty, value);
    }


    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);

        if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls

        if (e.AddedItems.Count > 0)
            if (!string.IsNullOrWhiteSpace(SelectedValuePath))
            {
                foreach (var item in e.AddedItems)
                    if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                        BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
            }
            else
            {
                foreach (var item in e.AddedItems)
                    if (!BindableSelectedItems.Contains((dynamic)item))
                        BindableSelectedItems.Add((dynamic)item);
            }

        if (e.RemovedItems.Count > 0)
            if (!string.IsNullOrWhiteSpace(SelectedValuePath))
            {
                foreach (var item in e.RemovedItems)
                    if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                        BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
            }
            else
            {
                foreach (var item in e.RemovedItems)
                    if (BindableSelectedItems.Contains((dynamic)item))
                        BindableSelectedItems.Remove((dynamic)item);
            }
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
        {
            List<dynamic> newSelection = new List<dynamic>();
            if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
                foreach (var item in listBox.BindableSelectedItems)
                {
                    foreach (var lbItem in listBox.Items)
                    {
                        var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
                        if ((dynamic)lbItemValue == (dynamic)item)
                            newSelection.Add(lbItem);
                    }
                }
            else
                newSelection = listBox.BindableSelectedItems as List<dynamic>;

            listBox.SetSelectedItems(newSelection);
        }
    }
}

La reliure fonctionne exactement comme vous vous en attendiez:

<uc:MultipleSelectionListBox 
    ItemsSource="{Binding Items}" 
    SelectionMode="Extended" 
    SelectedValuePath="id" 
    BindableSelectedItems="{Binding mySelection}"
/>

Il n'a pas été testé à fond mais a passé les inspections à première vue. J'ai essayé de le garder réutilisable en employant des types dynamiques sur les collections.

0
Wobbles

C'était assez facile à faire avec une commande et le EventTrigger d'Interactivities. ItemsCount est simplement une propriété liée à utiliser sur votre XAML, si vous souhaitez afficher le nombre mis à jour.

XAML:

     <ListBox ItemsSource="{Binding SomeItemsSource}"
                 SelectionMode="Multiple">
        <i:Interaction.Triggers>
         <i:EventTrigger EventName="SelectionChanged">
            <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" 
                                   CommandParameter="{Binding ElementName=MyView, Path=SelectedItems.Count}" />
         </i:EventTrigger>
        </Interaction.Triggers>    
    </ListView>

<Label Content="{Binding ItemsCount}" />

ViewModel:

    private int _itemsCount;
    private RelayCommand<int> _selectionChangedCommand;

    public ICommand SelectionChangedCommand
    {
       get {
                return _selectionChangedCommand ?? (_selectionChangedCommand = 
             new RelayCommand<int>((itemsCount) => { ItemsCount = itemsCount; }));
           }
    }

        public int ItemsCount
        {
            get { return _itemsCount; }
            set { 
              _itemsCount = value;
              OnPropertyChanged("ItemsCount");
             }
        }
0
str8ball

Voici encore une autre solution. C'est semblable à répondre de Ben, mais la liaison fonctionne de deux manières. L'astuce consiste à mettre à jour les éléments sélectionnés de ListBox lorsque les éléments de données liés changent.

public class MultipleSelectionListBox : ListBox
{
    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(IEnumerable<string>), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(IEnumerable<string>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public IEnumerable<string> BindableSelectedItems
    {
        get => (IEnumerable<string>)GetValue(BindableSelectedItemsProperty);
        set => SetValue(BindableSelectedItemsProperty, value);
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        BindableSelectedItems = SelectedItems.Cast<string>();
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
            listBox.SetSelectedItems(listBox.BindableSelectedItems);
    }
}

Malheureusement, je n'ai pas pu utiliser IList comme type BindableSelectedItems. Cela a envoyé null à la propriété de mon modèle de vue, dont le type est IEnumerable<string>.

Voici le XAML:

<v:MultipleSelectionListBox
    ItemsSource="{Binding AllMyItems}"
    BindableSelectedItems="{Binding MySelectedItems}"
    SelectionMode="Multiple"
    />

Il y a une chose à surveiller. Dans mon cas, un ListBox peut être supprimé de la vue. Pour une raison quelconque, la propriété SelectedItems devient une liste vide. À son tour, cela entraîne la modification de la propriété du modèle de vue en une liste vide. Selon votre cas d'utilisation, cela peut ne pas être souhaitable.

0
redcurry