0
votes

I'll post some code below, but here is a summary of What I am trying to do :

I have 2 viewmodels and 1 model :

  • ModelData - simple Model Class, just holds a string
  • ModelDataViewModel - ViewModel class with an ObservableCollection
  • MultipleCollectionsViewModel, ViewModel class with an ObservableCollection.

I have a 'parent' listbox, bound to the MultipleCollectionsViewModel. This listbox has SelectionMode=Extended. Each item represents a ModelDataViewModel, and any number of these can be selected.

I have a second 'child' listbox. This listbox should show all the ModelData objects of all selected items in the parent listbox. Basically, it shows a collection (ChildItems) of a collection (SelectedItems).

I got this to do what I want. But had quite a few issues. First off, SelectionMode=Extended just plain does not work out of box when binding to SelectedItem. The first item selected fires the property, subsequent selections don't. There are a lot of posts about this, I solved this by adding an IsSelected Style to the ListBoxItem. This gets applied to the 'ModelDataViewModel' objects.

The problem is that the parent view model, MultipleCollectionsViewModel, just doesn't have any clue that a property on one of its child viewModels got set. The correct PropertyChanged should be fired from MultipleCollectionsViewModel, not ModelDataViewModel.

I worked around this by adding an event. My understanding of MVVM is it should work around these events, and just rely on binding. But I have just been struggling bending over backwards trying to get this to work through binding/xaml alone. So, I'm posting this to see if anyone has any suggestions or different approaches on what I am trying to do.

Here is a screenshot with the multiple selections working :

Binding with SelectionMode=Extended

Code is below:

The ModelData class is very simple :

    public class ModelData
{
    public ModelData(string id)
    {
        this.Identifier = id;
    }

    public string Identifier
    {
        get;
        set;
    }
}

Here is the ModelDataViewModel

    public class ModelDataViewModel : ViewModelBase
{
    private ObservableCollection<ModelData> _aClassInstances;
    private string name;

    public ModelDataViewModel() : this("No-Name") { }

    public ModelDataViewModel(string name)
    {
        this.name = name;

        this.ChildItems = new ObservableCollection<ModelData>();
        this.ChildItems.Add(new ModelData(Name + "-1"));
        this.ChildItems.Add(new ModelData(Name + "-2"));
        this.ChildItems.Add(new ModelData(Name + "-3"));

        this.AVMIsSelected = false;
    }

    public ObservableCollection<ModelData> ChildItems
    {
        get
        {
            return this._aClassInstances;
        }
        set
        {
            this._aClassInstances = value;
            this.OnPropertyChanged("ChildItems");
        }
    }

    public string Name
    {
        get
        {
            return name;
        }

        set
        {
            name = value;
            this.OnPropertyChanged("Name");
        }
    }

    private bool isSelected;

    public bool AVMIsSelected
    {
        get
        {
            return this.isSelected;
        }
        set
        {
            this.isSelected = value;
            this.OnPropertyChanged("AVMIsSelected");         
        }
    }
}    

And the MultipleCollectionsViewModel :

    public class MultipleCollectionsViewModel : ViewModelBase
{
    ObservableCollection<ModelDataViewModel> firstItems;
    ObservableCollection<ModelDataViewModel> _selectedItems;     

    public MultipleCollectionsViewModel()
    {
        this.firstItems = new ObservableCollection<ModelDataViewModel>();            
        this._selectedItems = new ObservableCollection<ModelDataViewModel>();

        this.FirstItems.Add(new ModelDataViewModel("First-1"));
        this.FirstItems.Add(new ModelDataViewModel("First-2"));
        this.FirstItems.Add(new ModelDataViewModel("First-3"));
    }

    public ObservableCollection<ModelDataViewModel> FirstItems
    {
        get
        {
            return firstItems;
        }

        set
        {
            firstItems = value;
        }
    }

    public ObservableCollection<ModelDataViewModel> SelectedItems
    {
        get
        {
            return this._selectedItems;
        }

        set
        {                
            this._selectedItems.Clear();

            foreach (ModelDataViewModel aViewModel in this.FirstItems.Where(n => n.AVMIsSelected))
            {
                this._selectedItems.Add(aViewModel);
            }

            this.OnPropertyChanged("SelectedItems");
            this.OnPropertyChanged("SelectedSubItems");
        }
    }   

    public ObservableCollection<Model.ModelData> SelectedSubItems
    {
        get
        {
            return new ObservableCollection<Model.ModelData>(this.SelectedItems.SelectMany(n => n.ChildItems).Where(m => 1 == 1));
        }
    }

Finally, Here is the view and the code behind :

<Window x:Class="MVVM_Help_Needed.View.SelectedItemSync"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="SelectedItemSync" Height="500" Width="600">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>

    <ListBox Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" ItemsSource="{Binding Path=FirstItems}" SelectedItem="{Binding FirstSelectedItem, Mode=TwoWay}" SelectionMode="Extended" SelectionChanged="ListBox_SelectionChanged">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Path=Name}"></TextBlock>
            </DataTemplate>
        </ListBox.ItemTemplate>
        <ListBox.ItemContainerStyle>
            <Style TargetType="ListBoxItem">
                <Setter Property="IsSelected" Value="{Binding AVMIsSelected}"/>
            </Style>
        </ListBox.ItemContainerStyle>
    </ListBox>

    <Button Grid.Column="1" Grid.Row="0" Content="{Binding Path=SelectedItems.Count}"></Button>
    <ListBox Grid.Column="1" Grid.Row="1" ItemsSource="{Binding Path=SelectedSubItems}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Path=Identifier}"></TextBlock>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

    public partial class SelectedItemSync : Window
{
    public SelectedItemSync()
    {
        InitializeComponent();
        DataContext = new ViewModel.MultipleCollectionsViewModel();
    }

    private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ViewModel.MultipleCollectionsViewModel vm = this.DataContext as ViewModel.MultipleCollectionsViewModel;
        vm.SelectedItems = null; // this fires the setter, doesn't actually modify anything. 
    }
}
1

1 Answers

0
votes

Indeed this is a ListBox (and DataGrid) limitation. There are essentially 2 solutions: one using an attached property, the other enhances the ListBox control a little and adds the missing property. It is a matter of taste I prefer the second way because it gives me IntelliSense help. In your XAML you use MultiSelectionListBox instead of ListBox and you can bind to AllSelectedItems. Be aware this solution only observes changes/assignments of new list (set in the view model). And it always creates a new list when the selection changes. It depends on your needs whether you additionally want to observe an ObservableCollection in case that type is set in the view model.

public class MultiSelectionListBox : ListBox
{
  public static readonly DependencyProperty AllSelectedItemsProperty = DependencyProperty.Register(
    "AllSelectedItems",
    typeof(IList),
    typeof(MultiSelectionListBox),
    new FrameworkPropertyMetadata(null, OnAllSelectedItems));

  public MultiSelectionDataGrid()
  {
    this.SelectionChanged += this.MultiSelectionDataGridSelectionChanged;
  }

  public IList AllSelectedItems
  {
    get
    {
      return (IList)this.GetValue(AllSelectedItemsProperty);
    }

    set
    {
      this.SetValue(AllSelectedItemsProperty, value);
    }
  }

  private static void OnAllSelectedItems(object sender, DependencyPropertyChangedEventArgs e)
  {
    var me = (MultiSelectionListBox)sender;
    var newItemList = e.NewValue as IList;

    me.SetAllSelectedItems(newItemList);
  }

  private void MultiSelectionDataGridSelectionChanged(
    object sender, SelectionChangedEventArgs e)
  {
    IEnumerable<object> tempListSrc = this.SelectedItems.Cast<object>();
    var tempListDest = new List<object>();
    tempListDest.AddRange(tempListSrc.ToList());

    this.AllSelectedItems = tempListDest;
  }

  private void SetAllSelectedItems(IList list)
  {
    if (list == null)
    {
      return;
    }

    this.SelectedItems.Clear();

    foreach (object item in list)
    {
      this.SelectedItems.Add(item);
    }
  }
}