I have a ComboBox
with its ItemsSource
set to an IList
of MyClass
objects. I overrode the ComboBox
's ItemTemplate
to display a CheckBox
next to the items. I want to have an item at the top that says "Select All" and when the user checks that CheckBox
, the code checks all CheckBoxes
. My question is, what is the MVVM way of doing this?
I don't want to add a separate MyClass
object to the IList
. That seems like it would involve too much coupling of the view and the model. Is there a way to add an item directly in the XAML code and give it a Command
that checks all of the Checkboxes
?
My ComboBox
XAML right now is:
<ComboBox ItemsSource="{Binding MyList}" Width="200">
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding Selected}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
That looks like:
I'd like it to look like:
My MyClass is simply this:
public class MyClass
{
public string Name { get; set; }
public bool Selected { get; set; }
}
Edit: I found a way to add an item to the collection in the XAML code using the example here. I would still need a way run code when the user checks the checkbox for that "Select All" item. To just add the item, the code is this:
<ComboBox Width="200">
<ComboBox.Resources>
<CollectionViewSource x:Key="comboBoxSource" Source="{Binding Path=MyList}" />
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<local:MyClass Name="Select All" Selected="False">
</local:MyClass>
<CollectionContainer Collection="{Binding Source={StaticResource comboBoxSource}}" />
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding Selected}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Id personally just modify the CheckBox's Template and add custom CheckBox there with Click handler, nothing too fancy, easy to understand.
http://msdn.microsoft.com/en-us/library/ms752094(v=vs.110).aspx
From there you can modify this part:
<ScrollViewer Margin="4,6,4,6"
SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True"
KeyboardNavigation.DirectionalNavigation="Contained" />
</ScrollViewer>
The other way I would model is this simply as to create
public class MyClassViewModel
{
public string Name { get; set; }
public bool Selected { get; set; }
public ICommand Execute {get; set;}
}
and add custom object to your IList. This will work nicely, without any crap, your viewmodel has no idea about view, + it's testable. win-win for everyone.
Whenever your Button to select all is pressed you can invoke action, binding to itemssource, enumerating throughout collection setting Selected value to true. This solutiond requires Blend interface.
<local:MyClass Name="Select All" Selected="False">
<i:Interaction.Triggers>
<ic:DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Selected}" Value="True">
<local:SelectAll TargetObject="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=ItemsSource}"/>
</ic:DataTrigger>
</i:Interaction.Triggers>
</local:MyClass>
and then create class SelectAll as follows
public class SelectAll : TargetedTriggerAction<List<MyClass>>
{
protected override void Invoke(object parameter)
{
if (Target is List<MyClass>)
foreach (var elem in (List<MyClass>)Target)
elem.Selected = true;
}
}
I figured out a way to do it that uses some simpler concepts.
My first problem was that I wanted to add a "Select All" item to the top of the list without actually adding an item to the databound IList<MyClass>
. I edited my original post to show that I can use a CompositeCollection
to do this. I made the "Select All" databound object a subclass of MyClass
and called the class MyClassSelectAll
. More on this below.
My second problem was that I needed to handle the CheckBox.Click
event differently for the "Select All" item than for the regular MyClass
items. I gave each CheckBox
a Command
that I can use to check the CheckBoxes
. To do this, I need a reference to both the IList<MyClass>
and the CheckBox
on which the user clicked (more specfically the MyClass
object databound to that CheckBox
).
I found one way to solve my second problem is to set the CheckBox.CommandParameter
to a MultiBinding
that contains those two objects. In the Command.Execute(object)
method, I can easily check if the current MyClass
is a MyClassSelectAll
, and if so, I can loop through the IList<MyClass>
list (which doesn't contain the "Select All" item) and set the "Selected" property appropriately.
Here's my relevant XAML code:
<Grid.Resources>
<local:MyCommand x:Key="kMyCommand" />
<local:MyConverter x:Key="kConv" />
</Grid.Resources>
<ComboBox Width="200">
<ComboBox.Resources>
<CollectionViewSource x:Key="comboBoxSource" Source="{Binding Path=MyList}" />
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<local:MyClassSelectAll Name="Select All" Selected="False" />
<CollectionContainer Collection="{Binding
Source={StaticResource comboBoxSource}}" />
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}"
IsChecked="{Binding Selected}"
Command="{StaticResource kMyCommand}">
<CheckBox.CommandParameter>
<MultiBinding Converter="{StaticResource kConv}">
<Binding RelativeSource="{RelativeSource
FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.MyList" />
<Binding Path="." />
</MultiBinding>
</CheckBox.CommandParameter>
</CheckBox>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I updated MyClass
to implement INotifyPropertyChanged
appropriately. The code is simple, so I won't post it here.
MyConverter
is pretty simple as well; it just packages the list and current item object into a MyCommandArgs
object:
class MyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
MyCommandArgs result = new MyCommandArgs()
{
MyList = values[0] as IList<MyClass>,
CurrentItem = values[1] as MyClass
};
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The MyCommandArgs
class that I use to pass around the MyList
and CurrentItem
references is a simple container class so I won't post it here.
MyClassSelectAll
is the simplest; its only job is to flag the "Select All" MyClass
object as something that selects all.
class MyClassSelectAll : MyClass
{
}
Finally, The MyCommand
object that handles the click event.
class MyCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
var mylist = parameter as MyCommandArgs;
if (mylist != null && mylist.CurrentItem is MyClassSelectAll)
{
foreach (var item in mylist.MyList)
{
item.Selected = mylist.CurrentItem.Selected;
}
}
}
}
Thanks everyone. I was really stuck on this. It seems like many times when I have a problem doing something in WPF, the solution is to use a Converter
.
You have to registed the event when selecting a item, and when selecting the item,check that the item selected is the first one(using index) and then,change the selection to what do you need.
(Dont have the IDE here,sorry for not code).