16

I've got a WPF app using the Model-View-ViewModel pattern.
In my ViewModel I've got a ListCollectionView to keep a list of items.
This ListCollectionView is bound to a ListBox in my View.

<ListBox Grid.Row="1" ItemsSource="{Binding Useragents}" SelectionMode="Multiple"/>

The ListBox has SelectionMode=Multiple, so you can select more items at one time. Now the ViewModel needs to know which items has been selected.

The problem is: in the View-Model-ViewModel pattern the ViewModel has no access to the View, so I can't just ask the ListBox which items has been selected. All I have is the ListCollectionView, but I can't find a way to find which items has been selected in there.

So how do I find which items has been selected in the ListBox? Or a trick to achieve this (maybe bind something to a Boolean 'IsSelected' in my items? But what? How?)

Maybe someone who is using this pattern, too, can help me here?

Sam
  • 28,421
  • 49
  • 167
  • 247
  • I've asked this same question pretty much but haven't received an acceptable answer. http://stackoverflow.com/questions/1235772/how-to-best-represent-selectable-items-in-a-collection-within-the-m-v-vm-design-p – jpierson Jun 04 '10 at 14:40
  • 2
    This is totally ridiculous. While WPF is awesome in many aspects, there still are the nitty gritty basics that really do not work. How can an implementation leave out simple bindings of selectedItems to update the property in the view model. The chosen answer looks to me more like taking a 10,000 mile detour to get to your backyard. Things like this sometimes make wpf really weird to work with. – Matt Feb 26 '15 at 09:51
  • Yup, in some regards this sucks - but it is not WPF itself, but the binding stuff using M-V-VM. – Sam Feb 27 '15 at 15:35

9 Answers9

12

You need to create a ViewModel that has the concept of IsSelected on it and is bound to the IsSelected property of the actual ListBoxItem that represents it in the View using the standard WPF bindings architecture.

Then in your code, which knows about your ViewModel, but not the fact that it's represented by any specific View, can just use that property to find out which items from the Model are actually selected irrespective of the designers choice for how its represented in the View.

  • Sounds good, but how? Can you give me some more hints on how to achieve this with a ListBox and a ListCollectionView? – Sam Jan 16 '09 at 22:19
  • The collection you're going to bind to the LB's ItemsSource will contain instances of the VM class. Then you create a DataTemplate for this VM class which binds ListBoxItem.IsSelected to your class' IsSelected property. When the LB is populating, it will use that template automatically. –  Jan 17 '09 at 00:15
  • Well, I still got no clue how to do this, especially since (as I stated in my question) multiple items can be selected. – Sam May 25 '10 at 09:09
  • This answer is correct. See my answer for code sample of this. – surfen Dec 03 '11 at 17:34
9

PRISM MVVM Reference Implementation has a behaviour called SynchronizeSelectedItems, used in Prism4\MVVM RI\MVVM.Client\Views\MultipleSelectionView.xaml, which synchronizes checked items with the ViewModel property named Selections:

        <ListBox Grid.Column="0" Grid.Row="1" IsTabStop="False" SelectionMode="Multiple"
                 ItemsSource="{Binding Question.Range}" Margin="5">

            <ListBox.ItemContainerStyle>
                <!-- Custom style to show the multi-selection list box as a collection of check boxes -->
                <Style TargetType="ListBoxItem">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ListBoxItem">
                                <Grid Background="Transparent">
                                    <CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" 
                                              IsHitTestVisible="False" IsTabStop="True"
                                              AutomationProperties.AutomationId="CheckBoxAutomationId">
                                        <ContentPresenter/>
                                    </CheckBox>
                                </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ListBox.ItemContainerStyle>
            <i:Interaction.Behaviors>
                <!-- Custom behavior that synchronizes the selected items with the view models collection -->
                <Behaviors:SynchronizeSelectedItems Selections="{Binding Selections}"/>
            </i:Interaction.Behaviors>
        </ListBox>

Go to http://compositewpf.codeplex.com/ and grab it all or use this:

//===================================================================================
// Microsoft patterns & practices
// Composite Application Guidance for Windows Presentation Foundation and Silverlight
//===================================================================================
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===================================================================================
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace MVVM.Client.Infrastructure.Behaviors
{
    /// <summary>
    /// Custom behavior that synchronizes the list in <see cref="ListBox.SelectedItems"/> with a collection.
    /// </summary>
    /// <remarks>
    /// This behavior uses a weak event handler to listen for changes on the synchronized collection.
    /// </remarks>
    public class SynchronizeSelectedItems : Behavior<ListBox>
    {
        public static readonly DependencyProperty SelectionsProperty =
            DependencyProperty.Register(
                "Selections",
                typeof(IList),
                typeof(SynchronizeSelectedItems),
                new PropertyMetadata(null, OnSelectionsPropertyChanged));

        private bool updating;
        private WeakEventHandler<SynchronizeSelectedItems, object, NotifyCollectionChangedEventArgs> currentWeakHandler;

        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly",
            Justification = "Dependency property")]
        public IList Selections
        {
            get { return (IList)this.GetValue(SelectionsProperty); }
            set { this.SetValue(SelectionsProperty, value); }
        }

        protected override void OnAttached()
        {
            base.OnAttached();

            this.AssociatedObject.SelectionChanged += this.OnSelectedItemsChanged;
            this.UpdateSelectedItems();
        }

        protected override void OnDetaching()
        {
            this.AssociatedObject.SelectionChanged += this.OnSelectedItemsChanged;

            base.OnDetaching();
        }

        private static void OnSelectionsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = d as SynchronizeSelectedItems;

            if (behavior != null)
            {
                if (behavior.currentWeakHandler != null)
                {
                    behavior.currentWeakHandler.Detach();
                    behavior.currentWeakHandler = null;
                }

                if (e.NewValue != null)
                {
                    var notifyCollectionChanged = e.NewValue as INotifyCollectionChanged;
                    if (notifyCollectionChanged != null)
                    {
                        behavior.currentWeakHandler =
                            new WeakEventHandler<SynchronizeSelectedItems, object, NotifyCollectionChangedEventArgs>(
                                behavior,
                                (instance, sender, args) => instance.OnSelectionsCollectionChanged(sender, args),
                                (listener) => notifyCollectionChanged.CollectionChanged -= listener.OnEvent);
                        notifyCollectionChanged.CollectionChanged += behavior.currentWeakHandler.OnEvent;
                    }

                    behavior.UpdateSelectedItems();
                }
            }
        }

        private void OnSelectedItemsChanged(object sender, SelectionChangedEventArgs e)
        {
            this.UpdateSelections(e);
        }

        private void UpdateSelections(SelectionChangedEventArgs e)
        {
            this.ExecuteIfNotUpdating(
                () =>
                {
                    if (this.Selections != null)
                    {
                        foreach (var item in e.AddedItems)
                        {
                            this.Selections.Add(item);
                        }

                        foreach (var item in e.RemovedItems)
                        {
                            this.Selections.Remove(item);
                        }
                    }
                });
        }

        private void OnSelectionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            this.UpdateSelectedItems();
        }

        private void UpdateSelectedItems()
        {
            this.ExecuteIfNotUpdating(
                () =>
                {
                    if (this.AssociatedObject != null)
                    {
                        this.AssociatedObject.SelectedItems.Clear();
                        foreach (var item in this.Selections ?? new object[0])
                        {
                            this.AssociatedObject.SelectedItems.Add(item);
                        }
                    }
                });
        }

        private void ExecuteIfNotUpdating(Action execute)
        {
            if (!this.updating)
            {
                try
                {
                    this.updating = true;
                    execute();
                }
                finally
                {
                    this.updating = false;
                }
            }
        }
    }
}
surfen
  • 4,644
  • 3
  • 34
  • 46
  • 17
    For a moment I loled. Now i shiver in fear. So that's what it takes to figure out which items are selected in a listbox? I think some architect needs to die. – stmax Jan 28 '12 at 15:41
  • @stmax, wow, 100% my thoughts, I could not have said it better. It is probably hard to feel more cynical about those who hype WPF as the UI tool for the next 10 years. If developers cannot provide such simple functionality then I just cannot help but chuckle. This is not aimed at surfen but it makes me upset because I spent too much time on getting around this issue myself now. – Matt Feb 26 '15 at 09:54
  • Is there any way to do this using a regular `` instead of setting `ListBox.ItemContainerStyle.Template`? – Dai Sep 03 '15 at 23:26
  • @Dai: Yes, ItemContainerStyle.Template was used here just for custom presentation. This is not required. You only need to copy the behavior code file and reference it in xaml: – surfen Sep 13 '15 at 10:10
  • Don't you see the bug in the Microsoft's code, guys? It has this.AssociatedObject.SelectionChanged **+=** this.OnSelectedItemsChanged; in the OnDetaching() method. – Vitaly Sep 07 '16 at 00:28
1

Drew Marsh's answer is fine if you have a small list, if you have a large list the performance hit for finding all your selected items could be nasty! My favorite solution is to create an attached property on your ListBox that then binds to an ObservableCollection which contains your selected items. Then with your attached property you subscribe to the items SelectionChanged event to add/remove items from your collection.

David Rogers
  • 344
  • 2
  • 12
1

For me the best answer is to break a little the principle of MVVM.

On the code behind 1. Instanciate your viewModel 2. add an event handler SelectionChanged 3. iterate through your selected items and add each item to your list of the viewModel

ViewModel viewModel = new ViewModel();

viewModel.SelectedModules = new ObservableCollection<string>();

foreach (var selectedModule in listBox1.SelectedItems)
{
    viewModel.SelectedModules.Add(selectedModule.ToString());
}
ihebiheb
  • 3,673
  • 3
  • 46
  • 55
  • For me it seems to be a simple way to start breaking MVVM over and over again. Behavior solution is practically write once use all the time. The IsSelected property on the ViewModel is the one I have been using for a long time. One can create Selectable view model easily that adds an IsSelected property on top of T. – tomasz_kajetan_stanczak Sep 04 '13 at 15:52
1

Look at this blogpost by Josh Smith The Initially Selected Item when Binding to a Grouped ICollectionView

1

The solution of Drew Marsh works very well, I recommend it. And I have another solution !

Model View ViewModel is a Passive View, you can also use a Presentation Model to access some datas of your presentation without being coupled with WPF (this pattern is used in the Stocktrader example of PRISM).

Nicolas Dorier
  • 7,383
  • 11
  • 58
  • 71
0

Here is another variant of the View-Model-ViewModel Pattern where the ViewModel has access to the view through an IView interface.

I encountered quite a lot scenarios where you can't use WPF binding and then you need a way in code to synchronize the state between the View and the ViewModel.

How this can be done is shown here:

WPF Application Framework (WAF)

jbe
  • 6,976
  • 1
  • 43
  • 34
0

Have a look over here http://blog.functionalfun.net/2009/02/how-to-databind-to-selecteditems.html

0

David Rogers' solution is great and is detailed at the below related question:

Sync SelectedItems in a muliselect listbox with a collection in ViewModel

Community
  • 1
  • 1
zdv
  • 423
  • 3
  • 13