在MVVM中绑定DataGrid或ListBox的SelectedItems

37

最近在学习WPF,需要绑定DataGrid的选中项(selectedItems),但是始终无法得到实质性的进展。我只需要获取被选中的对象。

DataGrid:

<DataGrid Grid.Row="5" 
    Grid.Column="0" 
    Grid.ColumnSpan="4" 
    Name="ui_dtgAgreementDocuments"
    ItemsSource="{Binding Path=Documents, Mode=TwoWay}"
    SelectedItem="{Binding Path=DocumentSelection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    HorizontalAlignment="Stretch" 
    VerticalAlignment="Stretch" 
    Background="White"
    SelectionMode="Extended" Margin="2,5" 
    IsReadOnly="True" 
    CanUserAddRows="False" 
    CanUserReorderColumns="False" 
    CanUserResizeRows="False"
    GridLinesVisibility="None" 
    HorizontalScrollBarVisibility="Hidden"
    columnHeaderStyle="{StaticResource GreenTea}" 
    HeadersVisibility="Column" 
    BorderThickness="2" 
    BorderBrush="LightGray" 
    CellStyle="{StaticResource NonSelectableDataGridCellStyle}"
    SelectionUnit="FullRow" 
    HorizontalContentAlignment="Stretch" AutoGenerateColumns="False">
13个回答

75

SelectedItems可作为XAML的CommandParameter进行绑定。

经过大量挖掘和谷歌搜索,我终于找到了这个常见问题的简单解决方案。

为使其正常工作,您必须遵循以下所有规则

  1. 按照Ed Ball建议,在您的XAML命令数据绑定中,在Command属性之前定义CommandParameter属性。这是一个非常耗时的错误。

    enter image description here

  2. 确保您的ICommandCanExecuteExecute方法具有object类型的参数。这样,您可以防止出现静默转换异常,该异常会在数据绑定的CommandParameter类型与您的命令方法的参数类型不匹配时发生。

    private bool OnDeleteSelectedItemsCanExecute(object SelectedItems)  
    {
        // Your code goes here
    }
    
    private bool OnDeleteSelectedItemsExecute(object SelectedItems)  
    {
        // Your code goes here
    }
    

例如,您可以将列表视图/列表框的 SelectedItems 属性发送到您的 ICommand 方法或列表视图/列表框本身。很棒,不是吗?

希望这样做可以防止有人像我一样花费大量时间来找出如何将 SelectedItems 作为 CanExecute 参数接收。


我有一个 DataGrid,如果用户通过 InputBindingKeyBinding 访问其 ContextMenuMenuItemCommand,并且 CommandParameter="{Binding ElementName=MyDataGrid, Path=SelectedItems}",它将把 SelectedItems 传递给绑定的 ICommand。但是,如果通过 ContextMenu 访问,则传递 null。我尝试过 CommandParameter= "{Binding SelectedItems}""{Binding ElementName=MyDataGrid, Path=SelectedItems}""{Binding RelativeSource={RelativeSource Self}, Path=SelectedItems}"。是的,在 Command 之前设置了 CommandParameter - Tom
也尝试了"{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=SelectedItems}"。FYI,ContextMenu是通过<DataGrid.ContextMenu>内联定义的,然后是 ContextMenu - Tom
10
这段代码是可行的,但值得补充一下如何处理收到的对象。如果(parameter != null),则将(parameter)转换成(System.Collections.IList)类型的(items)列表,然后(selection)变量会将这个列表转化为指定类型(mydatatype)的选择集合。 - cjmurph
2
@Tom - 上下文菜单项不是可视树的一部分,因此您不能使用ElementName。请尝试使用CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource FindAncestor, AncestorType=ContextMenu}}" - Robin Bennett
+1 注意 CommandParameter 必须在 Command 之前。我最终放弃并不得不采用其他方法。有了这个想法再次尝试,它就可以工作了! - Adam L. S.
2
命令和命令参数的顺序并不重要。(至少在今天,已经使用 .net 5.0 进行了测试) <MenuItem Header="预览" Command="{Binding PreviewSelectedItemsCommand}" CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource FindAncestor, AncestorType=ContextMenu}}"/> - Kux

40

无法绑定到SelectedItems,因为它是只读属性。解决此问题的一种相对友好的MVVM方式是绑定到DataGridRowIsSelected属性。

可以按照以下方式设置绑定:

<DataGrid ItemsSource="{Binding DocumentViewModels}"
          SelectionMode="Extended">
    <DataGrid.Resources>
        <Style TargetType="DataGridRow">
            <Setter Property="IsSelected"
                    Value="{Binding IsSelected}" />
        </Style>
    </DataGrid.Resources>
</DataGrid>

然后,您需要创建一个继承自ViewModelBase(或您正在使用的任何MVVM基类)的DocumentViewModel,并具有您想要在DataGrid中呈现的Document的属性,以及一个IsSelected属性。
然后,在您的主视图模型中,创建一个名为DocumentViewModels的List(Of DocumentViewModel)来将您的DataGrid绑定到。(注意:如果您将向列表中添加/删除项目,请改用ObservableCollection(T)。)
现在,这是棘手的部分。您需要像这样钩入列表中每个DocumentViewModel的PropertyChanged事件:
For Each documentViewModel As DocumentViewModel In DocumentViewModels
    documentViewModel.PropertyChanged += DocumentViewModel_PropertyChanged
Next

这使您能够响应任何DocumentViewModel中的更改。

最后,在DocumentViewModel_PropertyChanged中,您可以循环遍历列表(或使用Linq查询)来获取每个项目的信息,其中IsSelected = True


其实我已经想到了,可以使用CommandParameter将文件从数据网格中传递。但是我只是需要知道它们是什么。 - Omar Mir
别误会 - 这样做更加清晰,也更符合MVVM的规范 - 我正在尝试找到最佳实现方式,因为有些其他对象使用文档集合的方式我不确定是否要将视图模型分离。它们并不依赖于彼此,但如果我可以从同一个VM中访问适当的属性,那么管道的处理就会变得更加容易,虽然我想我也可以使用两个单独的VM来实现这一点,你给了我一些思考的东西,丹,因为分离VM可能会使代码更加清晰... - Omar Mir
1
@OmarMir,总是存在权衡。如果你过度使用模式,最终只会为了微不足道的好处而做大量的管道工作。但是,如果你注意到事情开始感觉笨拙,那么现在可能是分离你的视图模型的时候了。 - devuxer
1
使用样式标签时,当数据表格中有滚动条时,绑定会被破坏,会出现非常奇怪的行为。我已经想出了一个可能更清晰的解决方案,详见底部。 - Omar Mir
2
Omar提到:“当有滚动条时,打破绑定”。我认为这是由于行虚拟化。您可以在DataGrid上设置EnableRowVirtualization="False"。否则,我同意这种方法行不通。 - Wallace Kelly
显示剩余5条评论

18

通过一些技巧,您可以扩展DataGrid来创建SelectedItems属性的可绑定版本。我的解决方案需要绑定具有Mode = OneWayToSource,因为我只想从该属性中读取,但是也许可以扩展我的解决方案以允许该属性为读写。

我认为类似的技术也可以用于ListBox,但我还没有尝试过。

public class BindableMultiSelectDataGrid : DataGrid
{
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register("SelectedItems", typeof(IList), typeof(BindableMultiSelectDataGrid), new PropertyMetadata(default(IList)));

    public new IList SelectedItems
    {
        get { return (IList)GetValue(SelectedItemsProperty); }
        set { throw new Exception("This property is read-only. To bind to it you must use 'Mode=OneWayToSource'."); }
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        SetValue(SelectedItemsProperty, base.SelectedItems);
    }
}

2
我将绑定设置为SelectedItems="{Binding SelectedSamples, Mode=OneWayToSource}"。但是在我的ViewModel属性的setter中,SelectedSamples的值始终为null,即使SelectedItems属性有一个或多个项目。我做错了什么? - blueshift
1
@blueshift 这里有同样的问题。你成功解决了吗? - Vereos
1
不,我无法让它正常工作。SelectedItems 始终为空。 - blueshift
1
它对我有用,尽管是在ListView而不是DataGrid上。 - JTennessen
4
我修复了它。我的问题是我在我的视图模型中使用了IList<MyType>并试图绑定它。这样是行不通的。你需要使用IList,并且不要在你的视图模型中调用new List<T>或者其他类似的东西,除非你有意在自己填充ListBox。否则,SetValue()会无限制地返回null。 - user99999991
显示剩余2条评论

8
这里有一个简单的解决方案。通过这种方式,您可以将/更新任何数据传递到ViewModel。
Designer.xaml
<DataGrid Grid.Row="1" Name="dgvMain" SelectionChanged="DataGrid_SelectionChanged" />

Designer.cs

ViewModel mModel = null;
public Designer()
{
    InitializeComponent();

    mModel = new ViewModel();
    this.DataContext = mModel;
}

private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    mModel.SelectedItems = dgvMain.SelectedItems;
}

ViewModel.cs

public class ViewModel
{
    public IList SelectedItems { get; set; }
}

1
这是一种简洁明了的方式,我认为使用Microsoft.Xaml.Behaviors.Wpf将SelectionChanged事件转换为命令(从而自动调用视图模型)可以更好地实现MVVM分离。 - Jonas

7

我来到这里是为了获取答案,现在我得到了很多好的回答。我将它们都合并到一个附加属性中,非常类似于Omar上面提供的那个,但在一个类中实现。处理INotifyCollectionChanged和切换列表。不会泄漏事件。我编写它以使代码更加简单易懂。使用C#编写,处理listbox selectedItems和dataGrid selectedItems。

这适用于DataGrid和ListBox。

(我刚学会如何使用GitHub) GitHub https://github.com/ParrhesiaJoe/SelectedItemsAttachedWpf

使用方法:

<ListBox ItemsSource="{Binding MyList}" a:Ex.SelectedItems="{Binding ObservableList}" 
         SelectionMode="Extended"/>
<DataGrid ItemsSource="{Binding MyList}" a:Ex.SelectedItems="{Binding OtherObservableList}" />

这是代码。在Git上有一个小样例。

public class Ex : DependencyObject
{
    public static readonly DependencyProperty IsSubscribedToSelectionChangedProperty = DependencyProperty.RegisterAttached(
        "IsSubscribedToSelectionChanged", typeof(bool), typeof(Ex), new PropertyMetadata(default(bool)));
    public static void SetIsSubscribedToSelectionChanged(DependencyObject element, bool value) { element.SetValue(IsSubscribedToSelectionChangedProperty, value); }
    public static bool GetIsSubscribedToSelectionChanged(DependencyObject element) { return (bool)element.GetValue(IsSubscribedToSelectionChangedProperty); }

    public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.RegisterAttached(
        "SelectedItems", typeof(IList), typeof(Ex), new PropertyMetadata(default(IList), OnSelectedItemsChanged));
    public static void SetSelectedItems(DependencyObject element, IList value) { element.SetValue(SelectedItemsProperty, value); }
    public static IList GetSelectedItems(DependencyObject element) { return (IList)element.GetValue(SelectedItemsProperty); }

    /// <summary>
    /// Attaches a list or observable collection to the grid or listbox, syncing both lists (one way sync for simple lists).
    /// </summary>
    /// <param name="d">The DataGrid or ListBox</param>
    /// <param name="e">The list to sync to.</param>
    private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is ListBox || d is MultiSelector))
            throw new ArgumentException("Somehow this got attached to an object I don't support. ListBoxes and Multiselectors (DataGrid), people. Geesh =P!");

        var selector = (Selector)d;
        var oldList = e.OldValue as IList;
        if (oldList != null)
        {
            var obs = oldList as INotifyCollectionChanged;
            if (obs != null)
            {
                obs.CollectionChanged -= OnCollectionChanged;
            }
            // If we're orphaned, disconnect lb/dg events.
            if (e.NewValue == null)
            {
                selector.SelectionChanged -= OnSelectorSelectionChanged;
                SetIsSubscribedToSelectionChanged(selector, false);
            }
        }
        var newList = (IList)e.NewValue;
        if (newList != null)
        {
            var obs = newList as INotifyCollectionChanged;
            if (obs != null)
            {
                obs.CollectionChanged += OnCollectionChanged;
            }
            PushCollectionDataToSelectedItems(newList, selector);
            var isSubscribed = GetIsSubscribedToSelectionChanged(selector);
            if (!isSubscribed)
            {
                selector.SelectionChanged += OnSelectorSelectionChanged;
                SetIsSubscribedToSelectionChanged(selector, true);
            }
        }
    }

    /// <summary>
    /// Initially set the selected items to the items in the newly connected collection,
    /// unless the new collection has no selected items and the listbox/grid does, in which case
    /// the flow is reversed. The data holder sets the state. If both sides hold data, then the
    /// bound IList wins and dominates the helpless wpf control.
    /// </summary>
    /// <param name="obs">The list to sync to</param>
    /// <param name="selector">The grid or listbox</param>
    private static void PushCollectionDataToSelectedItems(IList obs, DependencyObject selector)
    {
        var listBox = selector as ListBox;
        if (listBox != null)
        {
            if (obs.Count > 0)
            {
                listBox.SelectedItems.Clear();
                foreach (var ob in obs) { listBox.SelectedItems.Add(ob); }
            }
            else
            {
                foreach (var ob in listBox.SelectedItems) { obs.Add(ob); }
            }
            return;
        }
        // Maybe other things will use the multiselector base... who knows =P
        var grid = selector as MultiSelector;
        if (grid != null)
        {
            if (obs.Count > 0)
            {
                grid.SelectedItems.Clear();
                foreach (var ob in obs) { grid.SelectedItems.Add(ob); }
            }
            else
            {
                foreach (var ob in grid.SelectedItems) { obs.Add(ob); }
            }
            return;
        }
        throw new ArgumentException("Somehow this got attached to an object I don't support. ListBoxes and Multiselectors (DataGrid), people. Geesh =P!");
    }
    /// <summary>
    /// When the listbox or grid fires a selectionChanged even, we update the attached list to
    /// match it.
    /// </summary>
    /// <param name="sender">The listbox or grid</param>
    /// <param name="e">Items added and removed.</param>
    private static void OnSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var dep = (DependencyObject)sender;
        var items = GetSelectedItems(dep);
        var col = items as INotifyCollectionChanged;

        // Remove the events so we don't fire back and forth, then re-add them.
        if (col != null) col.CollectionChanged -= OnCollectionChanged;
        foreach (var oldItem in e.RemovedItems) items.Remove(oldItem);
        foreach (var newItem in e.AddedItems) items.Add(newItem);
        if (col != null) col.CollectionChanged += OnCollectionChanged;
    }

    /// <summary>
    /// When the attached object implements INotifyCollectionChanged, the attached listbox
    /// or grid will have its selectedItems adjusted by this handler.
    /// </summary>
    /// <param name="sender">The listbox or grid</param>
    /// <param name="e">The added and removed items</param>
    private static void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // Push the changes to the selected item.
        var listbox = sender as ListBox;
        if (listbox != null)
        {
            listbox.SelectionChanged -= OnSelectorSelectionChanged;
            if (e.Action == NotifyCollectionChangedAction.Reset) listbox.SelectedItems.Clear();
            else
            {
                foreach (var oldItem in e.OldItems) listbox.SelectedItems.Remove(oldItem);
                foreach (var newItem in e.NewItems) listbox.SelectedItems.Add(newItem);
            }
            listbox.SelectionChanged += OnSelectorSelectionChanged;
        }
        var grid = sender as MultiSelector;
        if (grid != null)
        {
            grid.SelectionChanged -= OnSelectorSelectionChanged;
            if (e.Action == NotifyCollectionChangedAction.Reset) grid.SelectedItems.Clear();
            else
            {
                foreach (var oldItem in e.OldItems) grid.SelectedItems.Remove(oldItem);
                foreach (var newItem in e.NewItems) grid.SelectedItems.Add(newItem);
            }
            grid.SelectionChanged += OnSelectorSelectionChanged;
        }
    }
 }

我有一个关于这个的问题。我已经实现了它,但由于它是一个DependencyProperty,我该如何将它传递到我的视图模型中?在提供的代码中,它只是在代码后台中。但是我已经可以在代码后台中访问SelectedItems。 - TheFaithfulLearner
1
这很好,谢谢。也许你应该提到必须先初始化集合。 - Thomas Klammer

6

我有一个解决方案,使用一个适合我的需求的变通方法。

ListItemTemplate上创建一个EventToCommand,在MouseUp事件中将SelectedItems集合作为CommandParameter发送。

 <i:Interaction.Triggers>
     <i:EventTrigger EventName="MouseUp">
         <helpers:EventToCommand Command="{Binding DataContext.SelectionChangedUpdate,
                                 RelativeSource={RelativeSource AncestorType=UserControl}}"
                                 CommandParameter="{Binding ElementName=personsList, Path=SelectedItems}" />
    </i:EventTrigger>                         
</i:Interaction.Triggers>

这样你就可以在视图模型中有一个命令来处理或保存所选项目以供以后使用。

愉快地编码吧!


你能否添加一个处理此问题的命令示例,就像你之前提到的那样? - Kappacake

5
对我来说,最简单的方法是在SelectionChanged事件中填充ViewModel属性。
private void MyDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    (DataContext as MyViewModel).SelectedItems.Clear();
    (DataContext as MyViewModel).SelectedItems.AddRange(MyDataGrid.SelectedItems.OfType<ItemType>());
}

一个务实的答案。谢谢。 - hillstuk
2
(DataContext as MyViewModel).SelectedItems.AddRange(MyDataGrid.SelectedItems.OfType<ItemType>()) - StayOnTarget

5

直接将绑定与视图模型关联,稍微有些棘手:

1)创建ICommand:

public class GetSelectedItemsCommand : ICommand
{
    public GetSelectedItemsCommand(Action<object> action)
    {
        _action = action;
    }

    private readonly Action<object> _action;

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _action(parameter);
    }
}

2) 创建数据表格

<DataGrid x:Name="DataGridOfDesperatePeople" SelectionMode="Extended">
    <i:Interaction.Triggers>
         <i:EventTrigger EventName="SelectionChanged">
              <i:InvokeCommandAction CommandParameter="{Binding ElementName=DataGridOfDesperatePeople, Path=SelectedItems}" Command="{Binding SelectedItemsCommand }" />
          </i:EventTrigger>
    </i:Interaction.Triggers>
</DataGrid>

3) 在视图模型中创建

public List<YourClassOfItemInTheGrid> SelectedItems { get; set; }  = new List<YourClassOfItemInTheGrid>();

public ICommand SelectedItemsCommand
{
    get
    {
        return new GetSelectedItemsCommand(list =>
        {

            SelectedItems.Clear();
            IList items = (IList)list;
            IEnumerable<YourClassOfItemInTheGrid> collection = items.Cast<YourClassOfItemInTheGrid>();
            SelectedItems = collection.ToList();
        });
    }
}

3
йңҖиҰҒдҪҝз”ЁNuGetеҢ…Microsoft.Xaml.Behaviors.Wpfе’Ңе‘ҪеҗҚз©әй—ҙxmlns:i="http://schemas.microsoft.com/xaml/behaviors"гҖӮ - Rob
+1s 给答案和 @Rob 的评论。它也适用于 ListBox。您可以在 XAML 中添加 <i:CallMethodAction TargetObject="{Binding}" MethodName="MapListBox_SelectionChanged"/>,然后在视图模型中创建一个名为 MapListBox_SelectionChanged 的公共方法,如果您喜欢使用视图模型中的 SelectionChanged 方法。 - Amadeus

4

我知道这篇帖子有点老了,也已经得到了答案。但是我想提供一个非MVVM的解决方案,它很简单并且适用于我。添加另一个DataGrid,假设您的选定集合是SelectedResults:

    <DataGrid x:Name="SelectedGridRows" 
        ItemsSource="{Binding SelectedResults,Mode=OneWayToSource}" 
        Visibility="Collapsed" >

在代码后台中,将以下内容添加到构造函数中:
    public ClassConstructor()
    {
        InitializeComponent();
        OriginalGrid.SelectionChanged -= OriginalGrid_SelectionChanged;
        OriginalGrid.SelectionChanged += OriginalGrid_SelectionChanged;
    }
    private void OriginalGrid_SelectionChanged(object sender, 
        SelectionChangedEventArgs e)
    {
        SelectedGridRows.ItemsSource = OriginalGrid.SelectedItems;
    }

4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接