2009-08-19 5 views
20

Ich habe eine Multi-Select-Listbox in einer SL3-App mit Prisma und ich brauche eine Sammlung in meinem Viewmodel, die die derzeit ausgewählten Elemente in der Listbox enthält.Sync SelectedItems in einem Multiselect-Listbox mit einer Sammlung in ViewModel

Das Ansichtsmodell weiß nichts über die Ansicht, so dass es keinen Zugriff auf das Listbox-Steuerelement hat. Außerdem muss ich in der Lage sein, die ausgewählten Elemente in der Listbox aus dem Viewmodel zu löschen.

nicht sicher, wie dieses Problem

dank Michael

Antwort

40

So nähern, übernehmen Sie eine Ansichtsmodell mit den folgenden Eigenschaften haben:

public ObservableCollection<string> AllItems { get; private set; } 
public ObservableCollection<string> SelectedItems { get; private set; } 

Sie würden beginnen, indem Sie Ihre AllItems Sammlung Bindung an die ListBox:

<ListBox x:Name="MyListBox" ItemsSource="{Binding AllItems}" SelectionMode="Multiple" /> 

Das Problem besteht darin, dass die SelectedItems-Eigenschaft in ListBox keine DependencyProperty ist. Das ist ziemlich schlecht, da Sie es nicht an etwas in Ihrem ViewModel binden können.

Der erste Ansatz ist nur in dem Code-Behind diese Logik zu setzen, das Ansichtsmodell zu optimieren:

public MainPage() 
{ 
    InitializeComponent(); 

    MyListBox.SelectionChanged += ListBoxSelectionChanged; 
} 

private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e) 
{ 
    var listBox = sender as ListBox; 
    if(listBox == null) return; 

    var viewModel = listBox.DataContext as MainVM; 
    if(viewModel == null) return; 

    viewModel.SelectedItems.Clear(); 

    foreach (string item in listBox.SelectedItems) 
    { 
     viewModel.SelectedItems.Add(item); 
    } 
} 

Dieser Ansatz funktioniert, aber es ist wirklich hässlich. Mein bevorzugter Ansatz besteht darin, dieses Verhalten in ein "Attached Behavior" zu extrahieren. Wenn Sie das tun, können Sie Ihren Code-Behind komplett beseitigen und im XAML einrichten. Der Bonus ist, dass diese "Befestigt Behavior" ist nun wieder verwendbar in jeder List-Box:

<ListBox ItemsSource="{Binding AllItems}" Demo:SelectedItems.Items="{Binding SelectedItems}" SelectionMode="Multiple" /> 

Und hier ist der Code für das angebaute Verhalten:

public static class SelectedItems 
{ 
    private static readonly DependencyProperty SelectedItemsBehaviorProperty = 
     DependencyProperty.RegisterAttached(
      "SelectedItemsBehavior", 
      typeof(SelectedItemsBehavior), 
      typeof(ListBox), 
      null); 

    public static readonly DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached(
      "Items", 
      typeof(IList), 
      typeof(SelectedItems), 
      new PropertyMetadata(null, ItemsPropertyChanged)); 

    public static void SetItems(ListBox listBox, IList list) { listBox.SetValue(ItemsProperty, list); } 
    public static IList GetItems(ListBox listBox) { return listBox.GetValue(ItemsProperty) as IList; } 

    private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    { 
     var target = d as ListBox; 
     if (target != null) 
     { 
      GetOrCreateBehavior(target, e.NewValue as IList); 
     } 
    } 

    private static SelectedItemsBehavior GetOrCreateBehavior(ListBox target, IList list) 
    { 
     var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior; 
     if (behavior == null) 
     { 
      behavior = new SelectedItemsBehavior(target, list); 
      target.SetValue(SelectedItemsBehaviorProperty, behavior); 
     } 

     return behavior; 
    } 
} 

public class SelectedItemsBehavior 
{ 
    private readonly ListBox _listBox; 
    private readonly IList _boundList; 

    public SelectedItemsBehavior(ListBox listBox, IList boundList) 
    { 
     _boundList = boundList; 
     _listBox = listBox; 
     _listBox.SelectionChanged += OnSelectionChanged; 
    } 

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) 
    { 
     _boundList.Clear(); 

     foreach (var item in _listBox.SelectedItems) 
     { 
      _boundList.Add(item); 
     } 
    } 
} 
+0

das funktioniert nicht mit SL3, hat das für jemand anderes funktioniert? – Neil

+1

das funktioniert überhaupt nicht, nicht nur weil die Klasse statisch sein sollte die ausgewählten Items sind immer Null. – msfanboy

+0

Dies funktioniert in SL3, SL4 und WPF. Ich benutze diese Methode die ganze Zeit. Ja, die Klasse mit dem angehängten Verhalten sollte statisch sein. Es ist Teil des "Attached Behavior" -Musters in SL und WPF. –

3

Dank dafür! Ich habe ein kleines Update hinzugefügt, um das anfängliche Laden und das Ändern von DataContext zu unterstützen.

Cheers,

Alessandro Pilotti [MVP/IIS]

public class SelectedItemsBehavior 
{ 
    private readonly ListBox _listBox; 
    private readonly IList _boundList; 

    public ListBoxSelectedItemsBehavior(ListBox listBox, IList boundList) 
    { 
     _boundList = boundList; 
     _listBox = listBox; 

     SetSelectedItems(); 

     _listBox.SelectionChanged += OnSelectionChanged; 
     _listBox.DataContextChanged += ODataContextChanged; 
    } 

    private void SetSelectedItems() 
    { 
     _listBox.SelectedItems.Clear(); 

     foreach (object item in _boundList) 
     { 
      // References in _boundList might not be the same as in _listBox.Items 
      int i = _listBox.Items.IndexOf(item); 
      if (i >= 0) 
       _listBox.SelectedItems.Add(_listBox.Items[i]); 
     } 
    } 

    private void ODataContextChanged(object sender, DependencyPropertyChangedEventArgs e) 
    { 
     SetSelectedItems(); 
    } 

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) 
    { 
     _boundList.Clear(); 

     foreach (var item in _listBox.SelectedItems) 
     { 
      _boundList.Add(item); 
     } 
    } 
} 
+0

ist es nur ich oder ist das Ereignis DataContextChanged nicht auf ListBox – Neil

+0

Es tut in WPF, aber nicht Silverlight bis Silverlight 5. –

1

Die ursprüngliche Lösung oben, wenn Sie arbeitet daran erinnern, zuerst eine Instanz der beobachtbaren Sammlung zu erstellen! Außerdem müssen Sie sicherstellen, dass der Inhaltstyp Observable collection mit dem Inhaltstyp für Ihre ListBox ItemSource übereinstimmt (wenn Sie von dem oben genannten Beispiel abweichen).

0

Die Lösung für mich war, Alessandro Pilotti Update mit Brian Genisio beigefügten Verhalten zu kombinieren. Entfernen Sie den Code für die Änderung von DataContext, aber Silverlight 4 unterstützt dies nicht.

Wenn Sie die Listbox an eine ObservableCollection<string> binden, funktioniert das oben gut, aber wenn Sie an komplexe Objekte wie ObservableCollection<Person> SelectedItems { get; private set; } über eine DataTemplate binden, scheint es nicht zu funktionieren. Dies ist aufgrund der Standardimplementierung der Equals Methode, die die Sammlung verwendet. Sie können dies lösen, indem Sie Ihrem Person-Objekt mitteilen, welche Felder verglichen werden sollen, wenn festgestellt wird, ob die Objekte gleich sind, indem Sie die Schnittstelle IEquatable<T> für Ihr Objekt implementieren.

Danach wird die IndexOf (Artikel) Code funktioniert und in der Lage sein zu vergleichen, wenn die Objekte gleich und wählen Sie den Eintrag in der Liste

// References in _boundList might not be the same as in _listBox.Items 
int i = _listBox.Items.IndexOf(item); 
if (i >= 0) 
    _listBox.SelectedItems.Add(_listBox.Items[i]); 

siehe Link sind: http://msdn.microsoft.com/en-us/library/ms131190(VS.95).aspx

5

Ich wollte eine echte bidirektionale Bindung, sodass die ListBox-Auswahl die Elemente in der SelectedItems-Auflistung des zugrunde liegenden ViewModels widerspiegelt. Dadurch kann ich die Auswahl nach Logik in der ViewModel-Ebene steuern.

Hier sind meine Änderungen an der SelectedItemsBehavior-Klasse. Sie synchronisieren die ListBox.SelectedItems-Auflistung mit der zugrunde liegenden ViewModel-Eigenschaft, wenn die ViewModel-Eigenschaft INotifyCollectionChanged implementiert (z. B. implementiert vom ObservableCollection-Typ T >).

public static class SelectedItems 
    { 
    private static readonly DependencyProperty SelectedItemsBehaviorProperty = 
     DependencyProperty.RegisterAttached(
      "SelectedItemsBehavior", 
      typeof(SelectedItemsBehavior), 
      typeof(ListBox), 
      null); 

    public static readonly DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached(
      "Items", 
      typeof(IList), 
      typeof(SelectedItems), 
      new PropertyMetadata(null, ItemsPropertyChanged)); 

    public static void SetItems(ListBox listBox, IList list) { listBox.SetValue(ItemsProperty, list); } 
    public static IList GetItems(ListBox listBox) { return listBox.GetValue(ItemsProperty) as IList; } 

    private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    { 
     var target = d as ListBox; 
     if (target != null) 
     { 
     AttachBehavior(target, e.NewValue as IList); 
     } 
    } 

    private static void AttachBehavior(ListBox target, IList list) 
    { 
     var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior; 
     if (behavior == null) 
     { 
     behavior = new SelectedItemsBehavior(target, list); 
     target.SetValue(SelectedItemsBehaviorProperty, behavior); 
     } 
    } 
    } 

    public class SelectedItemsBehavior 
    { 
    private readonly ListBox _listBox; 
    private readonly IList _boundList; 

    public SelectedItemsBehavior(ListBox listBox, IList boundList) 
    { 
     _boundList = boundList; 
     _listBox = listBox; 
     _listBox.Loaded += OnLoaded; 
     _listBox.DataContextChanged += OnDataContextChanged; 
     _listBox.SelectionChanged += OnSelectionChanged; 

     // Try to attach to INotifyCollectionChanged.CollectionChanged event. 
     var notifyCollectionChanged = boundList as INotifyCollectionChanged; 
     if (notifyCollectionChanged != null) 
     { 
     notifyCollectionChanged.CollectionChanged += OnCollectionChanged; 
     } 
    } 

    void UpdateListBoxSelection() 
    { 
     // Temporarily detach from ListBox.SelectionChanged event 
     _listBox.SelectionChanged -= OnSelectionChanged; 

     // Synchronize selected ListBox items with bound list 
     _listBox.SelectedItems.Clear(); 
     foreach (var item in _boundList) 
     { 
     // References in _boundList might not be the same as in _listBox.Items 
     var i = _listBox.Items.IndexOf(item); 
     if (i >= 0) 
     { 
      _listBox.SelectedItems.Add(_listBox.Items[i]); 
     } 
     } 

     // Re-attach to ListBox.SelectionChanged event 
     _listBox.SelectionChanged += OnSelectionChanged; 
    } 

    void OnLoaded(object sender, RoutedEventArgs e) 
    { 
     // Init ListBox selection 
     UpdateListBoxSelection(); 
    } 

    void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) 
    { 
     // Update ListBox selection 
     UpdateListBoxSelection(); 
    } 

    void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 
    { 
     // Update ListBox selection 
     UpdateListBoxSelection(); 
    } 

    void OnSelectionChanged(object sender, SelectionChangedEventArgs e) 
    { 
     // Temporarily deattach from INotifyCollectionChanged.CollectionChanged event. 
     var notifyCollectionChanged = _boundList as INotifyCollectionChanged; 
     if (notifyCollectionChanged != null) 
     { 
     notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; 
     } 

     // Synchronize bound list with selected ListBox items 
     _boundList.Clear(); 
     foreach (var item in _listBox.SelectedItems) 
     { 
     _boundList.Add(item); 
     } 

     // Re-attach to INotifyCollectionChanged.CollectionChanged event. 
     if (notifyCollectionChanged != null) 
     { 
     notifyCollectionChanged.CollectionChanged += OnCollectionChanged; 
     } 
    } 
    } 
+0

Ich kann nicht scheinen, dass das funktioniert: Ich wähle ein Element in der Listbox aus und führe dann einen Befehl aus, um dieses Element eine Position nach oben in der Liste zu verschieben. Dies geschieht, indem Sie es mit dem nächsten Element in der Sammlung austauschen, das an ItemsSource gebunden ist. Ich wähle dann den nächsten Artikel mit dem Code aus, den du gepostet hast. Endergebnis ist, dass sowohl das erste als auch das zweite Element in der Listbox ausgewählt sind, obwohl das _listBox.SelectedItems nur ein einzelnes Element enthält, wenn in UpdateListBoxSelection() ein Haltepunkt gesetzt wird. – stijn

+0

ok hab es geschafft: Zuerst musste ich die SelectedItems löschen, dann die ItemsSource ändern und erneut auswählen. Aus irgendeinem Grund zuerst Ändern und dann Ändern der Auswahl nicht richtig wokr .. – stijn

+0

Für diejenigen, die immer noch nicht zur Arbeit, stellen Sie sicher, dass Sie nicht Ihre Windows-Design-Farben wie ich ändern. Es stellte sich heraus, dass meine ListBox-Hintergrundfarbe mit der Auswahlfarbe übereinstimmte, wenn die ListBox nicht im Fokus war, so dass es so aussah, als ob nichts ausgewählt worden wäre. Ändern Sie Ihren ListBox-Hintergrundpinsel auf rot, um zu überprüfen, ob dies mit Ihnen geschieht. Es ließ mich 2 Stunden verbringen, bis ich realisierte ... – Kyopaxa

0

Ich benutze EventToCommand Objekt bei Auswahl geändert Ereignis in XAML und übergibt ListBox als Parameter. Der Befehl in MMVM verwaltet die ObservableCollection ausgewählter Elemente. Es ist einfach und schnell;)

0

Die Lösung von Brian Genisio und Samuel Jack here sind großartig. Ich habe es erfolgreich implementiert. Aber ich hatte auch einen Fall, in dem das nicht funktionierte und da ich kein Experte für WPF oder .Net bin, konnte ich es nicht debuggen. Ich bin mir immer noch nicht sicher, was das Problem ist, aber zu gegebener Zeit habe ich einen Workaround für die Multiselect-Bindung gefunden. Und in dieser Lösung musste ich nicht zum DataContext kommen.

Diese Lösung ist für Leute, die die obigen 2 Lösungen nicht funktionieren lassen können. Ich denke, diese Lösung wird nicht als MVVM betrachtet. Es geht so. Angenommen, Sie haben zwei Sammlungen in Ansichtsmodell:

public ObservableCollection<string> AllItems { get; private set; } 
public ObservableCollection<string> SelectedItems { get; private set; } 

Sie ein Listenfeld benötigen:

<ListBox x:Name="MyListBox" ItemsSource="{Binding AllItems}" SelectionMode="Multiple" /> 

nun eine weitere List-Box hinzu und binden Sie es an SelectedItems und setzen Sichtbarkeit:

<ListBox x:Name="MySelectedItemsListBox" ItemsSource="{Binding SelectedItems, Mode=OneWayToSource}" SelectionMode="Multiple" Visibility="Collapsed" /> 

Jetzt, Fügen Sie im Code hinter der WPF-Seite den Konstruktor nach der InitializeComponent() -Methode hinzu:

MyListBox.SelectionChanged += MyListBox_SelectionChanged; 

und fügen Sie eine Methode:

private void MyListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 
{ 
    MySelectedItemsListBox.ItemsSource = MyListBox.SelectedItems; 
} 

Und Sie fertig sind. Dies wird sicher funktionieren.Ich denke, das kann in Silverlight auch verwendet werden, wenn die obige Lösung nicht funktioniert.

0

Für diejenigen, die candritzky immer noch nicht zur Arbeit bringen konnten, stellen Sie sicher, dass Sie Ihre Windows-Designfarben nicht wie ich geändert haben. Es stellte sich heraus, dass meine ListBox-Hintergrundfarbe mit der Auswahlfarbe übereinstimmte, wenn die ListBox nicht im Fokus war, so dass es so aussah, als ob nichts ausgewählt worden wäre.

Ändern Sie Ihren ListBox-Hintergrundpinsel auf rot, um zu prüfen, ob dies mit Ihnen geschieht. Es ließ mich 2 Stunden verbringen, bis ich realisierte ...

Verwandte Themen