Remove SelectedItems from a ListBox via MVVM RelayCommand
-
25-09-2019 - |
문제
I have a list of items in a WPF ListBox. I want to allow the user to select several of these items and click a Remove button to eliminate these items from the list.
Using the MVVM RelayCommand pattern, I've created a command with the following signature:
public RelayCommand<IList> RemoveTagsCommand { get; private set; }
In my View, I wire up my RemoveTagsCommand like this:
<DockPanel>
<Button DockPanel.Dock="Right" Command="{Binding RemoveTagsCommand}" CommandParameter="{Binding ElementName=TagList, Path=SelectedItems}">Remove tags</Button>
<ListBox x:Name="TagList" ItemsSource="{Binding Tags}" SelectionMode="Extended">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Resources>
<DataTemplate DataType="{x:Type Model:Tag}">
...
</DataTemplate>
</ListBox.Resources>
</ListBox>
</DockPanel>
My ViewModel constructor sets up an instance of the command:
RemoveTagsCommand = new RelayCommand<IList>(RemoveTags, CanRemoveTags);
My current implementation of RemoveTags feels clunky, with casts and copying. Is there a better way to implement this?
public void RemoveTags(IList toRemove)
{
var collection = toRemove.Cast<Tag>();
List<Tag> copy = new List<Tag>(collection);
foreach (Tag tag in copy)
{
Tags.Remove(tag);
}
}
해결책
This looks fairly clean to me, though you might be able to bind SelectedItems
to a property on your VM using Mode=OneWayToSource
and then use the bound collection property from RemoveTags
.
I'm not entirely sure, but you may be able to use a strongly-typed IList collection in this case.
다른 팁
I'd use the ItemContainerStyle
on the ListBox
to bind the items' IsSelected
property to a flag in the Model (not View Model), e.g.:
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
Then you don't need to worry about what argument you're passing in to your command. Also, in my experience when it's simple for an object in a view model to know that the user selected it, you find other uses for that information.
The code in the command would be something like:
foreach (Tag t in Tags.Where(x => x.IsSelected).ToList())
{
Tags.Remove(t);
}
Why don't you specify the type argument of the RelayCommand
to be a List<Tag>
, since that is what you are going to get anyway? There is no point specifying a more generic type than that because the executed handler is hard-coded to work with a list of Tag
objects. Since you've already made the dependency there, you might as well make it on the type argument, as well. Then your executed handler won't need any cast or copying.
1.) Bind your Remove button a Command in your ViewModel.
2.) When you set up your binding, use a CommandParameter to take the Selecteditems from your ListBox by giving your ListBox a name and using ElementName=NameOfListBox, Path=SelectedItems
3.) Make sure your Command in your ViewModel passes along the arguments. You'll get an object that you can cast as an IList.
Below is a simple example, this should help you set up your structure.
In the View:
<Button Command="{Binding CommandInViewModelForRemove}"
CommandParameter="{Binding ElementName=blah,Path=SelectedItems}"
<ListBox x:Name="blah" .... />
In the ViewModel:
public ViewModel(){
RemoveCommand = new RelayCommand<object>(Remove, CanRemove);
}
private void Remove(object selectedItems){
var list = (IList)selectedItems;
//do some work, cast to view models that represent list items, etc
}
Hope this helps!