it-swarm.com.de

So unterstützen Sie die ListBox SelectedItems-Bindung mit MVVM in einer navigierbaren Anwendung

Ich mache eine WPF-Anwendung, die mit benutzerdefinierten Schaltflächen und Befehlen "Weiter" und "Zurück" navigierbar ist (d. H. Keine NavigationWindow). Auf einem Bildschirm habe ich eine ListBox, die mehrere Auswahlen unterstützen muss (mit dem Extended-Modus). Ich habe ein Ansichtsmodell für diesen Bildschirm und speichere die ausgewählten Elemente als Eigenschaft, da sie gepflegt werden müssen.

Mir ist jedoch bekannt, dass die SelectedItems-Eigenschaft einer ListBox schreibgeschützt ist. Ich habe versucht, das Problem mithilfe von dieser Lösung hier zu umgehen, aber ich konnte es nicht in meine Implementierung übernehmen. Ich habe festgestellt, dass ich nicht unterscheiden kann, wann ein oder mehrere Elemente abgewählt sind und ich zwischen den Bildschirmen navigiere (NotifyCollectionChangedAction.Remove wird in beiden Fällen ausgelöst, da technisch alle ausgewählten Elemente beim Navigieren vom Bildschirm weg deaktiviert werden). Meine Navigationsbefehle befinden sich in einem separaten Ansichtsmodell, das die Ansichtsmodelle für jeden Bildschirm verwaltet. Daher kann ich keine Implementierung für das Ansichtsmodell mit der Variable ListBox dort einfügen.

Ich habe mehrere andere weniger elegante Lösungen gefunden, aber keine davon scheint eine wechselseitige Bindung zwischen dem Ansichtsmodell und der Ansicht zu erzwingen.

Jede Hilfe wäre sehr dankbar. Ich kann einen Teil meines Quellcodes zur Verfügung stellen, wenn dies zum Verständnis des Problems beitragen würde.

30
caseklim

Erstellen Sie für jedes Ihrer Datenelemente eine IsSelected -Eigenschaft, und binden Sie ListBoxItem.IsSelected an diese Eigenschaft

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

Rachels Lösungen funktionieren großartig! Es gibt jedoch ein Problem, auf das ich gestoßen bin: Wenn Sie den Stil von ListBoxItem überschreiben, verlieren Sie den ursprünglichen Stil, der darauf angewendet wurde (in meinem Fall für das Hervorheben des ausgewählten Elements usw.). Sie können dies vermeiden, indem Sie vom ursprünglichen Stil erben:

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

Beachten Sie die Einstellung BasedOn (siehe diese Antwort ) .

17
Mifeet

Ich konnte Rachels Lösung nicht so bearbeiten, wie ich es wollte, aber ich fand die Antwort von Sandesh , eine benutzerdefinierte dependency-Eigenschaft zu erstellen, die perfekt für mich funktioniert. Ich musste nur einen ähnlichen Code für eine ListBox schreiben:

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

}

In meinem Ansichtsmodell habe ich nur auf diese Eigenschaft verwiesen, um meine ausgewählte Liste zu erhalten.

8
Ben

Ich habe immer nach einer einfachen Lösung gesucht, aber ohne Glück.

Die Lösung, die Rachel hat, ist gut, wenn Sie bereits die Selected-Eigenschaft für das Objekt in Ihrer ItemsSource haben. Wenn Sie dies nicht tun, müssen Sie ein Modell für dieses Geschäftsmodell erstellen.

Ich bin einen anderen Weg gegangen. Schnell, aber nicht perfekt.

Erstellen Sie in Ihrer ListBox ein Ereignis für SelectionChanged.

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

Implementieren Sie nun das Ereignis im Code hinter Ihrer XAML-Seite.

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

Tada Erledigt.

Dies wurde mit Hilfe von Konvertierung von SelectedItemCollection in eine Liste durchgeführt.

2
AzzamAziz

Ich war mit den gegebenen Antworten nicht zufrieden, ich versuchte selbst eine zu finden ... Nun, es stellt sich heraus, dass es eher ein Hack als eine Lösung ist, aber für mich funktioniert das gut. Diese Lösung verwendet MultiBindings auf besondere Weise. Zunächst mag es wie eine Tonne Code aussehen, aber Sie können es mit sehr wenig Aufwand wiederverwenden.

Zuerst habe ich einen 'IMultiValueConverter' implementiert

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

Und ein SelectedItems Container/Wrapper:

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

Jetzt erstellen wir die Bindung für unser ListBox.SelectedItem (Singular). Hinweis: Sie müssen eine statische Ressource für den 'Konverter' erstellen. Dies kann einmal pro Anwendung erfolgen und für alle ListBoxen, die den Konverter benötigen, wiederverwendet werden.

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

Im ViewModel habe ich den Container erstellt, an den ich mich binden kann. Es ist wichtig, es mit new () zu initialisieren, um es mit den Werten zu füllen.

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

Und das ist es. Vielleicht sieht jemand Verbesserungen? Was denkst du darüber?

0
Fresch

Dies war ein Hauptproblem für mich, einige der Antworten, die ich gesehen habe, waren entweder zu hackig oder das Zurücksetzen des SelectedItems-Eigenschaftswerts war erforderlich, wobei der mit dem OnCollectionChanged -Ereignis verknüpfte Code gebrochen wurde. Es gelang mir jedoch, eine praktikable Lösung zu erhalten, indem ich die Sammlung direkt modifizierte und als Bonus sogar SelectedValuePath für Objektsammlungen unterstützt.

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

Die Bindung funktioniert genau so, wie Sie es von MS erwartet hätten:

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

Es wurde nicht gründlich getestet, hat jedoch erste Überprüfungen bestanden. Ich habe versucht, es durch die Verwendung dynamischer Typen für die Sammlungen wiederverwendbar zu halten.

0
Wobbles

Dies war ziemlich einfach mit einem Command und dem Interactivities EventTrigger zu tun. ItemsCount ist nur eine gebundene Eigenschaft, die in Ihrer XAML verwendet werden soll, wenn Sie die aktualisierte Anzahl anzeigen möchten.

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

Hier ist noch eine andere Lösung. Es ist ähnlich wie Bens answer , aber die Bindung funktioniert auf zwei Arten. Der Trick besteht darin, die ausgewählten Elemente von ListBox zu aktualisieren, wenn sich die gebundenen Datenelemente ändern.

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

Leider konnte ich IList nicht als BindableSelectedItems-Typ verwenden. Dadurch wurde null an die Eigenschaft meines Ansichtsmodells gesendet, dessen Typ IEnumerable<string> ist.

Hier ist die XAML:

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

Es gibt eine Sache, auf die Sie achten sollten. In meinem Fall kann ein ListBox aus der Ansicht entfernt werden. Aus irgendeinem Grund ändert sich die SelectedItems-Eigenschaft in eine leere Liste. Dies bewirkt wiederum, dass die Eigenschaft des Ansichtsmodells in eine leere Liste geändert wird. Abhängig von Ihrem Anwendungsfall ist dies möglicherweise nicht wünschenswert.

0
redcurry