Pavel Murygin
Pavel Murygin

Reputation: 2312

WPF: binding a list of all options and a list of selected options to listbox with choice checkboxes

Let's say I have a list (editable) of some options. I want to have a maintainable list of items that a user has picked. I want the following UI for picking items: ListBox of pairs "option name", "ispicked". I hope ASCII art below will make things more clear:

 All cars            Select!          Selected cars
____________    _________________    ____________
|BMW       |    |BMW       | [x] |   |BMW       | 
|Audi      |    |Audi      | [x] |   |Audi      | 
|Volkswagen|    |Volkswagen| [x] |   |Volkswagen| 
|Honda     |    |Honda     | [ ] |   |          | 
|Toyota    |    |Toyota    | [ ] |   |          | 
|Nissan    |    |Nissan    | [ ] |   |          | 
|Ford      |    |Ford      | [ ] |   |          | 
|__________|    |__________|_____|   |__________|

If a user marks "Nissan" checkbox everything should like:

 All cars            Select!          Selected cars
____________    _________________    ____________
|BMW       |    |BMW       | [x] |   |BMW       | 
|Audi      |    |Audi      | [x] |   |Audi      | 
|Volkswagen|    |Volkswagen| [x] |   |Volkswagen| 
|Honda     |    |Honda     | [ ] |   |Hond      | 
|Toyota    |    |Toyota    | [ ] |   |Nissan    | 
|Nissan    |    |Nissan    | [x] |   |          | 
|Ford      |    |Ford      | [ ] |   |          | 
|__________|    |__________|_____|   |__________|

Adding "Nissan" to collection of selected cars should mark corresponding checkbox in the middle ListBox. Removing an Item from the collection of all cars should remove it's name from second and third ListBoxes. E.g. removing Volkswagen from the second picture should result in:

 All cars            Select!          Selected cars
____________    _________________    ____________
|BMW       |    |BMW       | [x] |   |BMW       | 
|Audi      |    |Audi      | [x] |   |Audi      | 
|Honda     |    |Honda     | [ ] |   |Hond      | 
|Toyota    |    |Toyota    | [ ] |   |Nissan    | 
|Nissan    |    |Nissan    | [x] |   |          | 
|Ford      |    |Ford      | [ ] |   |          | 
|__________|    |__________|_____|   |__________|

Here is some viewmodel code:

[ImplementPropertyChanged]
public class ChoiceViewModel : ViewModelBase
{
    public string Title { get; set; }
    public bool IsChecked { get; set; }
}

[ImplementPropertyChanged]
public class TestViewModel : ViewModelBase
{
    public ObservableCollection<string> AllOptions { get; set; }
    public ObservableCollection<string> SelectedOptions { get; set; }

    public ObservableCollection<ChoiceViewModel> Choices { get; set; }

    public TestViewModel()
    {
        AllOptions = new ObservableCollection<string>();
        SelectedOptions = new ObservableCollection<string>();
        Choices = new ObservableCollection<ChoiceViewModel>();

        if(IsInDesignMode)
        {
            AllOptions.Add("BMW");
            AllOptions.Add("Audi");
            AllOptions.Add("Volkswagen");
            AllOptions.Add("Honda");
            AllOptions.Add("Toyota");
            AllOptions.Add("Nissan");
            AllOptions.Add("Ford");

            // German cars ftw!
            SelectedOptions.Add("BMW");
            SelectedOptions.Add("Audi");
            SelectedOptions.Add("Volkswagen");
        }
    }
}

And the XAML for window:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
    DataContext="{Binding TestViewModel">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="22" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <TextBlock Text="All cars" Grid.Row="0" Grid.Column="0" />
        <DataGrid ItemsSource="{Binding AllOptions}" Grid.Row="1" Grid.Column="0" Margin="10" 
                  AutoGenerateColumns="False"
                  CanUserAddRows="True" 
                  CanUserDeleteRows="True">
            <DataGrid.Columns>
                <DataGridTextColumn Width="1*" Binding="{Binding}"/>
            </DataGrid.Columns>
        </DataGrid>

        <TextBlock Text="Select!" Grid.Row="0" Grid.Column="1" />
        <ListBox ItemsSource="{Binding Choices}" Grid.Row="1" Grid.Column="1" Margin="10" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Title}" />
                        <CheckBox IsChecked="{Binding IsChecked}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <TextBlock Text="Selected cars" Grid.Row="0" Grid.Column="2" />
        <DataGrid ItemsSource="{Binding SelectedOptions}" Grid.Row="1" Grid.Column="2" Margin="10" 
                  AutoGenerateColumns="False"
                  CanUserAddRows="True" 
                  CanUserDeleteRows="True">
            <DataGrid.Columns>
                <DataGridTextColumn Width="1*" Binding="{Binding}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

So the question is how to sync AllOptions, SelectedOptions and Choices collections WPF\MVVM style?

Edit: I've ended up with the accepted answer. The limitation of SelectedItems binding can be circumvented either by making ListBoxEx wrapper with additional BindableSelectedItems dependency property exposed or by using attached properteis. See this question for details

Upvotes: 2

Views: 512

Answers (1)

McGarnagle
McGarnagle

Reputation: 102743

Obviously there are multiple way to approach this, but I would be inclined to use a single collection -- that way you don't need to sync at all.

  1. The first list would be the same as you have it, bound to "AllOptions"
  2. Also bind the second list to "AllOptions". To make this work, you would need to use a "check-box list" template for the ListBox.
  3. Then simply bind the third to the second list's SelectedItems property.

The tricky part of this is creating the check-box style list, but I believe that is simpler than syncing 2 or 3 separate lists and the sub-viewmodel.

Edit

Below is a simple example.

<Grid>
    <Grid.Resources>
        <Style TargetType="ListBox" x:Key="CheckBoxListStyle">
            <Setter Property="SelectionMode" Value="Multiple" />
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>
                            <TextBlock Text="{Binding}" />
                            <CheckBox Grid.Column="1"
                                      IsChecked="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},Path=IsSelected,Mode=TwoWay}"
                                      />
                        </Grid>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Grid.Resources>
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <ListBox ItemsSource="{Binding AllOptions}" />

    <ListBox x:Name="selectionList"
             Grid.Column="1"
             ItemsSource="{Binding AllOptions}" Style="{StaticResource CheckBoxListStyle}"
             />

    <ListBox Grid.Column="2"
             ItemsSource="{Binding ElementName=selectionList,Path=SelectedItems}"
             />
</Grid>

Edit #2

I like this solution less now that I see the "SelectedItems" list does not keep the original sorting.

Upvotes: 1

Related Questions