Search code examples
c#wpfxamlmvvmdata-binding

Returning bound checkbox values using MVVM in a WPF form


I have an object that consists of a string and an array. The string populates a ComboBox and the array populates a ListView depending on the selected string value. Each line of the ListViewconsists of a TextBlock and a CheckBox.

On submit I want to be able to verify which items have been selected for further processing but there's a disconnect when using the MVVM approach. I currently have the DataContext of the submit Button binding to the ListView but only the first value is being returned upon submit (somewhere I need to save the selected values to a list I assume but I'm not sure where). I added an IsSelected property to the model which I think is the first step, but after that I've been grasping at straws.

Model

namespace DataBinding_WPF.Model
{
    public class ExampleModel { }

    public class Example : INotifyPropertyChanged
    {
        private string _name;
        private string[] _ids;
        private bool _isSelected;

        public bool IsSelected
        {
            get => _isSelected;
            set
            {
                if (_isSelected != value)
                {
                    _isSelected = value;
                    RaisePropertyChanged("IsSelected");
                }
            }
        }

        public string Name
        {
            get => _name;
            set
            {
                if (_name != value)
                {
                    _name = value;
                    RaisePropertyChanged("Name");
                }
            }
        }

        public string[] IDs
        {
            get => _ids;
            set
            {
                if (_ids != value)
                {
                    _ids = value;
                    RaisePropertyChanged("IDs");
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new 
                    PropertyChangedEventArgs(property));
            }
        }
    }
}

ViewModel

namespace DataBinding_WPF.ViewModel
{
    public class ExampleViewModel : INotifyPropertyChanged
    {
        public ObservableCollection<Example> Examples
        {
            get;
            set;
        }

        // SelectedItem in the ComboBox
        // SelectedItem.Ids will be ItemsSource for the ListBox
        private Example _selectedItem;
        public Example SelectedItem
        {
            get => _selectedItem;
            set
            {
                _selectedItem = value;
                RaisePropertyChanged(nameof(SelectedItem));
            }
        }

        // SelectedId in ListView
        private string _selectedId;
        public string SelectedId
        {
            get => _selectedId;
            set
            {
                _selectedId = value;
                RaisePropertyChanged(nameof(SelectedId));
            }
        }

        private string _selectedCheckBox;
        public string IsSelected
        {
            get => _selectedCheckBox;
            set
            {
                _selectedCheckBox = value;
                RaisePropertyChanged(nameof(IsSelected));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new
                    PropertyChangedEventArgs(property));
            }
        }

        public void LoadExample()  
        {
            ObservableCollection<Example> examples = new ObservableCollection<Example>();
            examples.Add(new Example { Name = "Mark", IDs = new string[] { "123", "456" }, IsSelected = false });
            examples.Add(new Example { Name = "Sally", IDs = new string[] { "789", "101112" }, IsSelected = false });
            Examples = examples;
        }

        /* BELOW IS A SNIPPET I ADDED FROM AN EXAMPLE I FOUND ONLINE BUT NOT SURE IF IT'S NEEDED */
        private ObservableCollection<Example> _bindCheckBox;
        public ObservableCollection<Example> BindingCheckBox
        {
            get => _bindCheckBox;
            set
            {
                _bindCheckBox = value;
                RaisePropertyChanged("BindingCheckBox");
            }
        }
    }
}

View

<UserControl x:Class = "DataBinding_WPF.Views.StudentView" 
   xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml" 
   xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006" 
   xmlns:d = "http://schemas.microsoft.com/expression/blend/2008" 
   xmlns:local = "clr-namespace:DataBinding_WPF" 
   mc:Ignorable = "d" 
   d:DesignHeight = "300" d:DesignWidth = "300">

    <Grid>
        <StackPanel HorizontalAlignment = "Left" >

            <ComboBox HorizontalAlignment="Left"   
                  VerticalAlignment="Top"   
                  Width="120"   
                  ItemsSource="{Binding Path=Examples}"    
                  SelectedItem="{Binding SelectedItem}"  
                  DisplayMemberPath="Name"/>

            <ListView x:Name="myListView" 
                        ItemsSource="{Binding SelectedItem.IDs}"
                        DataContext="{Binding DataContext, ElementName=submit_btn}"
                        SelectedItem="{Binding SelectedId}" 
                        Height="200" Margin="10,50,0,0" 
                              Width="Auto"
                        VerticalAlignment="Top"
                        Background="AliceBlue">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal" >
                            <CheckBox 
                                    Name="myCheckBox"                            
                                    IsChecked="{Binding IsSelected,
                                        RelativeSource={RelativeSource AncestorType=ListViewItem}}"
                                        Margin="5, 0"/>
                            <TextBlock Text="{Binding}" FontWeight="Bold" />
                        </StackPanel>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>

            <Button HorizontalAlignment="Left" Height="20" Width="100"
                    Click="Submit" x:Name="submit_btn">Submit</Button>
        </StackPanel>
    </Grid>
</UserControl>

View.cs

namespace DataBinding_WPF.Views
{
    /// <summary>
    /// Interaction logic for StudentView.xaml
    /// </summary>
    public partial class StudentView : UserControl
    {
        public StudentView()
        {
            InitializeComponent();
        }

        private void Submit(object sender, EventArgs e)
        {
            var selectedItems = ((Button)sender).DataContext;

            // process each selected item
           // foreach (var selected in ....) { }
        }
    }
}

Solution

  • The ListView control already exposes a selected items collection as property SelectedItems.

    private void Submit(object sender, RoutedEventArgs e)
    {
       var selectedIds = myListView.SelectedItems.Cast<string>().ToList();
       // ...do something with the items.
    }
    

    However, I doubt that you want to do this in the code-behind, but rather in the view model. For this purpose, WPF offers the concept of commands.

    What you need is a relay command or delegate command (the name varies across frameworks). It encapsulates a method that should be executed for e.g. a button click and a method to determine whether the command can be executed as an object that can be bound in the view. Unfortunately, WPF does not provide an implementation out-of-the-box, so you either have to copy an implementation like here or use an MVVM framework that already provides one, e.g. Microsoft MVVM Tookit.

    You would expose a property Submit of type ICommand in your ExampleViewModel and initialize it in the constructor with an instance of RelayCommand<T> that delegates to a method to execute.

    public class ExampleViewModel : INotifyPropertyChanged
    {
       public ExampleViewModel()
       {
          Submit = new RelayCommand<IList>(ExecuteSubmit);
       }
    
       public RelayCommand<IList> Submit { get; }
    
       // ...other code.
    
       private void ExecuteSubmit(IList selectedItems)
       {
          // ...do something with the items.
          var selectedIds = selectedItems.Cast<string>().ToList();
          return;
       }
    }
    

    In your view, you would remove the Click event handler and bind the Submit property to the Command property of the Button. You can also bind the SelectedItems property of the ListView to the CommandParameter property, so the selected items are passed to the command on execution.

    <Button HorizontalAlignment="Left"
            Height="20"
            Width="100"
            x:Name="submit_btn"
            Command="{Binding Submit}"
            CommandParameter="{Binding SelectedItems, ElementName=myListView}">Submit</Button>
    

    Additionally, a few remarks about your XAML.

    • Names of controls in XAML should be Pascal-Case, starting with a capital letter.

    • You should remove the DataContext binding from ListView completely, as it automatically receives the same data context as the Button anyway.

      DataContext="{Binding DataContext, ElementName=submit_btn}"
      
    • You can save yourself from exposing and binding the SelectedItem property in your ExampleViewModel, by using Master/Detail pattern for hierarchical data.

      <Grid>
         <StackPanel HorizontalAlignment = "Left" >
      
            <ComboBox HorizontalAlignment="Left"   
                      VerticalAlignment="Top"   
                      Width="120"   
                      ItemsSource="{Binding Path=Examples}"   
                      IsSynchronizedWithCurrentItem="True"
                      DisplayMemberPath="Name"/>
      
            <ListView ItemsSource="{Binding Examples/IDs}"
                      SelectedItem="{Binding SelectedId}" 
                      Height="200" Margin="10,50,0,0" 
                      Width="Auto"
                      VerticalAlignment="Top"
                      Background="AliceBlue">
               <ListView.ItemTemplate>
                  <DataTemplate>
                     <StackPanel Orientation="Horizontal" >
                        <CheckBox Name="myCheckBox"                            
                                  IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}"
                                  Margin="5, 0"/>
                        <TextBlock Text="{Binding}"
                                   FontWeight="Bold" />
                     </StackPanel>
                  </DataTemplate>
               </ListView.ItemTemplate>
            </ListView>
      
            <Button HorizontalAlignment="Left"
                    Height="20"
                    Width="100"
                    Command="{Binding Submit}"
                    CommandParameter="{Binding SelectedItems, ElementName=myListView}">Submit</Button>
      
         </StackPanel>
      </Grid>