可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
There are some posts discussing adding data-binding ability for ListView.SelectedItems
with non-trivial amount of code. In my scenario I don't need to set it from the ViewModel
, just getting selected items in order to perform action on them and it is triggered by command so push update is also not necessary.
Is there a simple solution (in terms of lines of code), maybe in code-behind? I am fine with code-behind as long as View
and ViewModel
don't need to reference each other. I think this is a more generic question: "best practice for VM to get data from the View on-demand", but I can't seem to find anything...
回答1:
To get the SelectedItems
only when a command is executed then use CommandParameter
and pass in the ListView.SelectedItems
.
<ListBox x:Name="listbox" ItemsSource="{Binding StringList}" SelectionMode="Multiple"/>
<Button Command="{Binding GetListItemsCommand}" CommandParameter="{Binding SelectedItems, ElementName=listbox}" Content="GetSelectedListBoxItems"/>
回答2:
This can be achieved using Interaction triggers as below
You will need to add reference to
Microsoft.Expression.Interactions
System.Windows.Interactivity
Add below xmlns to your xaml
xmlns:i="http://schemas.microsoft.com/expression//2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Add code below just inside your GridView tag
<GridView x:Name="GridName">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding Datacontext.SelectionChangedCommand, ElementName=YourUserControlName}" CommandParameter="{Binding SelectedItems, ElementName=GridName}" />
</i:EventTrigger>
</i:Interaction.Triggers>
Code Inside ViewModel declare property below
public DelegateCommand<object> SelectionChangedCommand {get;set;}
within constructor of Viewmodel initialize Command as below
SelectionChangedCommand = new DelegateCommand<object> (items => {
var itemList = (items as ObservableCollection<object>).Cast<YourDto>().ToList();
}
回答3:
I don't think it's correct condition to consider that 'View and ViewModel don't need to know each other'; In MVVM view always know about ViewModel.
I have also come across this kind of situation where I had to access ViewModel in view's code behind and then populate some data(like selected items), this becomes necessary while using 3'rd party controls like ListView, DataGrid etc.
If directly binding the VM property is not possible then I would listen to ListViw.SelectionChanged event and then update my ViewModels SelectedItems property in that event.
Update:
To enable VM pull data from view, You can expose an interface on the View that handles View-specific functionality and ViewModel will have reference of your View through that interface; Using an interface still keeps the View and ViewModel largely decoupled but I genrally don't prefer this.
MVVM, providing the Association of View to ViewModel
I would still prefer the approch of handling the event in View and keep the VM updated(with the selected items), this way VM don't need to worry about pulling the data before performing any operation, it just needs to use the data available(as that will always be updated one).
回答4:
I can assure you: SelectedItems is indeed bindable as a XAML CommandParameter
After a lot of digging and googling, I have finally found a simple solution to this common issue.
To make it work you must follow ALL the following rules:
Following Ed Ball's suggestion', on you XAML command databinding, define CommandParameter property BEFORE Command property. This a very time-consuming bug.
Make sure your ICommand's CanExecute and Execute methods have a parameter of object type. This way you can prevent silenced cast exceptions that occurs whenever databinding CommandParameter type does not match your command method's parameter type.
private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)
{
// Your goes heres
}
private bool OnDeleteSelectedItemsExecute(object SelectedItems)
{
// Your goes heres
}
For example, you can either send a listview/listbox's SelectedItems property to you ICommand methods or the listview/listbox it self. Great, isn't it?
Hope it prevents someone spending the huge amount of time I did to figure out how to receive SelectedItems as CanExecute parameter.
回答5:
Since none of the other answers helped me (using SelectedItems
as CommandParameter
was always null
), here is a solution for Universal Windows Platform (UWP) apps. It works using Microsoft.Xaml.Interactivity
and Microsoft.Xaml.Interactions.Core
.
Here's the View:
<ListView x:Name="ItemsList">
<Interactivity:Interaction.Behaviors>
<Core:EventTriggerBehavior EventName="SelectionChanged">
<Core:InvokeCommandAction Command="{x:Bind ViewModel.SelectedItemsChanged}" />
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
<!-- content etc. -->
</ListView>
Here's the ViewModel (RelayCommand
is a class from MVVM Light):
private List<YourType> _selectedItems = new List<YourType>();
private RelayCommand<SelectionChangedEventArgs> _selectedItemsChanged;
public RelayCommand<SelectionChangedEventArgs> SelectedItemsChanged
{
get
{
if (_selectedItemsChanged == null)
_selectedItemsChanged = new RelayCommand<SelectionChangedEventArgs>((selectionChangedArgs) =>
{
// add a guard here to immediatelly return if you are modifying the original collection from code
foreach (var item in selectionChangedArgs.AddedItems)
_selectedItems.Add((YourType)item);
foreach (var item in selectionChangedArgs.RemovedItems)
_selectedItems.Remove((YourType)item);
});
return _selectedItemsChanged;
}
}
Beware that if you are going to remove items from the original collection after the selection is completed (user pushes a button etc.), it will also remove the items from your _selectedItems
list! If you do this in a foreach loop, you'll get an InvalidOperationException
. To avoid this, simply add a guard in the marked place like:
if (_deletingItems)
return;
and then in the method where you for example remove the items, do this:
_deletingItems = true;
foreach (var item in _selectedItems)
YourOriginalCollection.Remove(item);
_deletingItems = false;