【问题标题】:Bind to SelectedItems from DataGrid or ListBox in MVVM从 MVVM 中的 DataGrid 或 ListBox 绑定到 SelectedItems
【发布时间】:2012-04-10 10:56:52
【问题描述】:

只是在 WPF 上做一些简单的阅读,我需要从 DataGrid 绑定 selectedItems,但我无法想出任何有形的东西。我只需要选定的对象。

数据网格:

<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">

【问题讨论】:

    标签: c# wpf vb.net xaml mvvm


    【解决方案1】:

    SelectedItems 可绑定为 XAML CommandParameter

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

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

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

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

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

    例如,您可以将 listview/listbox 的 SelectedItems 属性发送给您的 ICommand 方法或它自己的 listview/listbox。很好,不是吗?

    希望它可以防止有人花费大量时间来弄清楚如何接收 SelectedItems 作为 CanExecute 参数。

    【讨论】:

    • 我有一个DataGrid,如果用户通过InputBindingKeyBinding 访问其ContextMenuMenuItemCommand 之一,其@987654330 @,它会将SelectedItems 传递给绑定ICommand。但是,如果通过ContextMenu 访问null,则会传递null。我试过CommandParameter="{Binding SelectedItems}""{Binding ElementName=MyDataGrid, Path=SelectedItems}""{Binding RelativeSource={RelativeSource Self}, Path=SelectedItems}"。是的,我在Command 之前设置了CommandParameter
    • 也试过"{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Path=SelectedItems}"。仅供参考,ContextMenu 是通过 &lt;DataGrid.ContextMenu&gt;ContextMenu 内联定义的。
    • 这行得通,但值得添加如何处理你得到的对象。 if (parameter != null) { System.Collections.IList items = (System.Collections.IList)parameter; var selection = items?.Cast(); }
    • @Tom - 上下文菜单项不是可视化树的一部分,因此您不能使用 ElementName。试试CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource FindAncestor,AncestorType=ContextMenu}}"
    • +1 表示 CommandParameter 必须在 Command 之前。我最终放弃了,不得不求助于其他方法。考虑到这一点,再次尝试它,它的工作原理!
    【解决方案2】:

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

    您可以像这样设置绑定:

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

    然后,您需要创建一个 DocumentViewModel,它继承自 ViewModelBase(或您正在使用的任何 MVVM 基类)并具有您想要在 DataGrid 中显示的 Document 的属性,以及一个 @ 987654328@财产。

    然后,在您的主视图模型中,您创建一个名为 DocumentViewModelsList(Of DocumentViewModel) 以将您的 DataGrid 绑定到。 (注意:如果您要从列表中添加/删除项目,请改用ObservableCollection(T)。)

    现在,这是棘手的部分。您需要挂钩列表中每个DocumentViewModelPropertyChanged 事件,如下所示:

    For Each documentViewModel As DocumentViewModel In DocumentViewModels
        documentViewModel.PropertyChanged += DocumentViewModel_PropertyChanged
    Next
    

    这允许您响应任何DocumentViewModel 中的更改。

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

    【讨论】:

    • 实际上只是使用 CommandParameter 弄清楚了,我可以从数据网格中传递文件。但是是的,我只需要知道它们是什么。
    • @OmarMir,我看到您已经找到了解决方案。请参阅我的编辑以获取替代方案。
    • @OmarMir,总会有取舍。如果你过度使用这些模式,你最终只会做大量的管道工作而没有太多好处。但是,如果您发现事情开始变得杂乱无章,那么可能是时候分离您的视图模型了。
    • 使用样式标签最终会在数据网格中有滚动条时破坏绑定,开始出现非常奇怪的行为。我在底部想出了一个可能更清洁的解决方案。
    • Omar 提到,“当有滚动条时打破绑定”。我认为这是因为行虚拟化。您可以在 DataGrid 上设置 EnableRowVirtualization="False"。否则,我同意这种方法行不通。
    【解决方案3】:

    通过一些技巧,您可以扩展 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);
        }
    }
    

    【讨论】:

    • 我将绑定设置为 SelectedItems="{Binding SelectedSamples, Mode=OneWayToSource}"。但是在我的 ViewModel 属性 SelectedSamples 的设置器中,值始终为 null,即使 SelectedItems 属性具有一个或多个项目。我做错了什么?
    • @blueshift 同样的问题在这里。你设法让它工作了吗?
    • 不,我无法让它工作。 SelectedItems 始终为空。
    • 它对我有用,但在 ListView 而不是 DataGrid 上。
    • 我修好了。所以我的问题是我在我的视图模型中使用 IList 并尝试绑定它。这行不通。您需要使用 IList 并且不要在视图模型中调用 new List 或其他内容,除非您有意尝试自己填充 ListBox。否则,SetValue() 将无限期吐出 null。
    【解决方案4】:

    我来这里是为了答案,我得到了很多很棒的答案。我将它们全部组合成一个附加属性,与上面 Omar 提供的非常相似,但在一个类中。处理 INotifyCollectionChanged 并切换列表。不泄漏事件。我写了它,所以它是非常简单的代码。用 C# 编写,处理列表框 selectedItems 和 dataGrid selectedItems。

    这适用于 DataGrid 和 ListBox。

    (我刚刚学会了如何使用 GitHub) GitHubhttps://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 了。
    • 这很好,谢谢。也许你应该提到你必须先初始化集合。
    【解决方案5】:

    这是一个简单的解决方案。这样您就可以将任何数据传递/更新到 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; }
    }
    

    【讨论】:

    • 好又干净的方式,我认为这应该更好地使用Microsoft.Xaml.Behaviors.Wpf将SelectionChanged事件转换为命令(并因此让它自动调用视图模型) .
    【解决方案6】:

    直接绑定做view model,小窍门版:

    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();
            });
        }
    }
    

    【讨论】:

    • 需要 nuget 包 Microsoft.Xaml.Behaviors.Wpf 和命名空间 xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    • +1 回答和@Rob 评论。它也适用于 ListBox。如果您喜欢在视图模型中使用 SelectionChanged 方法,您可以在 XAML 中添加 然后在视图模型中创建一个公共方法 MapListBox_SelectionChanged
    【解决方案7】:

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

    ListItemTemplate 上创建EventToCommand MouseUp 并作为CommandParameter 发送SelectedItems 集合

     <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>
    

    这样,您可以在视图模型中使用命令来处理此问题或保存所选项目以供以后使用。 玩得开心编码

    【讨论】:

    • 您能否添加一个您提到的处理此问题的命令示例?
    【解决方案8】:

    我知道这篇文章有点老了,已经得到了回答。但我想出了一个非 MVVM 答案。它很简单,对我有用。添加另一个 DataGrid,假设您的 Selected Collection 是 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;
        }
    

    【讨论】:

      【解决方案9】:

      对我来说,最简单的方法是在 SelectionChanged 事件中填充 ViewModel 属性。

      private void MyDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
      {
          (DataContext as MyViewModel).SelectedItems.Clear();
          foreach (var i in MyDataGrid.SelectedItems) (DataContext as MyViewModel).SelectedItems.Add(i as ItemType);
      }
      

      【讨论】:

      • 一个务实的答案。谢谢。
      • (DataContext as MyViewModel).SelectedItems.AddRange(MyDataGrid.SelectedItems.OfType&lt;ItemType&gt;())
      【解决方案10】:

      这将起作用:

      MultiSelectorBehaviours.vb

      Imports System.Collections
      Imports System.Windows
      Imports System.Windows.Controls.Primitives
      Imports System.Windows.Controls
      Imports System
      
      Public NotInheritable Class MultiSelectorBehaviours
          Private Sub New()
          End Sub
      
          Public Shared ReadOnly SynchronizedSelectedItems As DependencyProperty = _
              DependencyProperty.RegisterAttached("SynchronizedSelectedItems", GetType(IList), GetType(MultiSelectorBehaviours), New PropertyMetadata(Nothing, New PropertyChangedCallback(AddressOf OnSynchronizedSelectedItemsChanged)))
      
          Private Shared ReadOnly SynchronizationManagerProperty As DependencyProperty = DependencyProperty.RegisterAttached("SynchronizationManager", GetType(SynchronizationManager), GetType(MultiSelectorBehaviours), New PropertyMetadata(Nothing))
      
          ''' <summary>
          ''' Gets the synchronized selected items.
          ''' </summary>
          ''' <param name="dependencyObject">The dependency object.</param>
          ''' <returns>The list that is acting as the sync list.</returns>
          Public Shared Function GetSynchronizedSelectedItems(ByVal dependencyObject As DependencyObject) As IList
              Return DirectCast(dependencyObject.GetValue(SynchronizedSelectedItems), IList)
          End Function
      
          ''' <summary>
          ''' Sets the synchronized selected items.
          ''' </summary>
          ''' <param name="dependencyObject">The dependency object.</param>
          ''' <param name="value">The value to be set as synchronized items.</param>
          Public Shared Sub SetSynchronizedSelectedItems(ByVal dependencyObject As DependencyObject, ByVal value As IList)
              dependencyObject.SetValue(SynchronizedSelectedItems, value)
          End Sub
      
          Private Shared Function GetSynchronizationManager(ByVal dependencyObject As DependencyObject) As SynchronizationManager
              Return DirectCast(dependencyObject.GetValue(SynchronizationManagerProperty), SynchronizationManager)
          End Function
      
          Private Shared Sub SetSynchronizationManager(ByVal dependencyObject As DependencyObject, ByVal value As SynchronizationManager)
              dependencyObject.SetValue(SynchronizationManagerProperty, value)
          End Sub
      
          Private Shared Sub OnSynchronizedSelectedItemsChanged(ByVal dependencyObject As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
              If e.OldValue IsNot Nothing Then
                  Dim synchronizer As SynchronizationManager = GetSynchronizationManager(dependencyObject)
                  synchronizer.StopSynchronizing()
      
                  SetSynchronizationManager(dependencyObject, Nothing)
              End If
      
              Dim list As IList = TryCast(e.NewValue, IList)
              Dim selector As Selector = TryCast(dependencyObject, Selector)
      
              ' check that this property is an IList, and that it is being set on a ListBox
              If list IsNot Nothing AndAlso selector IsNot Nothing Then
                  Dim synchronizer As SynchronizationManager = GetSynchronizationManager(dependencyObject)
                  If synchronizer Is Nothing Then
                      synchronizer = New SynchronizationManager(selector)
                      SetSynchronizationManager(dependencyObject, synchronizer)
                  End If
      
                  synchronizer.StartSynchronizingList()
              End If
          End Sub
      
          ''' <summary>
          ''' A synchronization manager.
          ''' </summary>
          Private Class SynchronizationManager
              Private ReadOnly _multiSelector As Selector
              Private _synchronizer As TwoListSynchronizer
      
              ''' <summary>
              ''' Initializes a new instance of the <see cref="SynchronizationManager"/> class.
              ''' </summary>
              ''' <param name="selector">The selector.</param>
              Friend Sub New(ByVal selector As Selector)
                  _multiSelector = selector
              End Sub
      
              ''' <summary>
              ''' Starts synchronizing the list.
              ''' </summary>
              Public Sub StartSynchronizingList()
                  Dim list As IList = GetSynchronizedSelectedItems(_multiSelector)
      
                  If list IsNot Nothing Then
                      _synchronizer = New TwoListSynchronizer(GetSelectedItemsCollection(_multiSelector), list)
                      _synchronizer.StartSynchronizing()
                  End If
              End Sub
      
              ''' <summary>
              ''' Stops synchronizing the list.
              ''' </summary>
              Public Sub StopSynchronizing()
                  _synchronizer.StopSynchronizing()
              End Sub
      
              Public Shared Function GetSelectedItemsCollection(ByVal selector As Selector) As IList
                  If TypeOf selector Is MultiSelector Then
                      Return TryCast(selector, MultiSelector).SelectedItems
                  ElseIf TypeOf selector Is ListBox Then
                      Return TryCast(selector, ListBox).SelectedItems
                  Else
                      Throw New InvalidOperationException("Target object has no SelectedItems property to bind.")
                  End If
              End Function
      
          End Class
      End Class
      

      IListItemConverter.vb

      ''' <summary>
      ''' Converts items in the Master list to Items in the target list, and back again.
      ''' </summary>
      Public Interface IListItemConverter
          ''' <summary>
          ''' Converts the specified master list item.
          ''' </summary>
          ''' <param name="masterListItem">The master list item.</param>
          ''' <returns>The result of the conversion.</returns>
          Function Convert(ByVal masterListItem As Object) As Object
      
          ''' <summary>
          ''' Converts the specified target list item.
          ''' </summary>
          ''' <param name="targetListItem">The target list item.</param>
          ''' <returns>The result of the conversion.</returns>
          Function ConvertBack(ByVal targetListItem As Object) As Object
      End Interface
      

      TwoListSynchronizer.vb

      Imports System.Collections
      Imports System.Collections.Specialized
      Imports System.Linq
      Imports System.Windows
      
      ''' <summary>
      ''' Keeps two lists synchronized. 
      ''' </summary>
      Public Class TwoListSynchronizer
          Implements IWeakEventListener
      
          Private Shared ReadOnly DefaultConverter As IListItemConverter = New DoNothingListItemConverter()
          Private ReadOnly _masterList As IList
          Private ReadOnly _masterTargetConverter As IListItemConverter
          Private ReadOnly _targetList As IList
      
      
          ''' <summary>
          ''' Initializes a new instance of the <see cref="TwoListSynchronizer"/> class.
          ''' </summary>
          ''' <param name="masterList">The master list.</param>
          ''' <param name="targetList">The target list.</param>
          ''' <param name="masterTargetConverter">The master-target converter.</param>
          Public Sub New(ByVal masterList As IList, ByVal targetList As IList, ByVal masterTargetConverter As IListItemConverter)
              _masterList = masterList
              _targetList = targetList
              _masterTargetConverter = masterTargetConverter
          End Sub
      
          ''' <summary>
          ''' Initializes a new instance of the <see cref="TwoListSynchronizer"/> class.
          ''' </summary>
          ''' <param name="masterList">The master list.</param>
          ''' <param name="targetList">The target list.</param>
          Public Sub New(ByVal masterList As IList, ByVal targetList As IList)
              Me.New(masterList, targetList, DefaultConverter)
          End Sub
      
          Private Delegate Sub ChangeListAction(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
      
          ''' <summary>
          ''' Starts synchronizing the lists.
          ''' </summary>
          Public Sub StartSynchronizing()
              ListenForChangeEvents(_masterList)
              ListenForChangeEvents(_targetList)
      
              ' Update the Target list from the Master list
              SetListValuesFromSource(_masterList, _targetList, AddressOf ConvertFromMasterToTarget)
      
              ' In some cases the target list might have its own view on which items should included:
              ' so update the master list from the target list
              ' (This is the case with a ListBox SelectedItems collection: only items from the ItemsSource can be included in SelectedItems)
              If Not TargetAndMasterCollectionsAreEqual() Then
                  SetListValuesFromSource(_targetList, _masterList, AddressOf ConvertFromTargetToMaster)
              End If
          End Sub
      
          ''' <summary>
          ''' Stop synchronizing the lists.
          ''' </summary>
          Public Sub StopSynchronizing()
              StopListeningForChangeEvents(_masterList)
              StopListeningForChangeEvents(_targetList)
          End Sub
      
          ''' <summary>
          ''' Receives events from the centralized event manager.
          ''' </summary>
          ''' <param name="managerType">The type of the <see cref="T:System.Windows.WeakEventManager"/> calling this method.</param>
          ''' <param name="sender">Object that originated the event.</param>
          ''' <param name="e">Event data.</param>
          ''' <returns>
          ''' true if the listener handled the event. It is considered an error by the <see cref="T:System.Windows.WeakEventManager"/> handling in WPF to register a listener for an event that the listener does not handle. Regardless, the method should return false if it receives an event that it does not recognize or handle.
          ''' </returns>
          Public Function ReceiveWeakEvent(ByVal managerType As Type, ByVal sender As Object, ByVal e As EventArgs) As Boolean Implements System.Windows.IWeakEventListener.ReceiveWeakEvent
              HandleCollectionChanged(TryCast(sender, IList), TryCast(e, NotifyCollectionChangedEventArgs))
      
              Return True
          End Function
      
          ''' <summary>
          ''' Listens for change events on a list.
          ''' </summary>
          ''' <param name="list">The list to listen to.</param>
          Protected Sub ListenForChangeEvents(ByVal list As IList)
              If TypeOf list Is INotifyCollectionChanged Then
                  CollectionChangedEventManager.AddListener(TryCast(list, INotifyCollectionChanged), Me)
              End If
          End Sub
      
          ''' <summary>
          ''' Stops listening for change events.
          ''' </summary>
          ''' <param name="list">The list to stop listening to.</param>
          Protected Sub StopListeningForChangeEvents(ByVal list As IList)
              If TypeOf list Is INotifyCollectionChanged Then
                  CollectionChangedEventManager.RemoveListener(TryCast(list, INotifyCollectionChanged), Me)
              End If
          End Sub
      
          Private Sub AddItems(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
              Dim itemCount As Integer = e.NewItems.Count
      
              For i As Integer = 0 To itemCount - 1
                  Dim insertionPoint As Integer = e.NewStartingIndex + i
      
                  If insertionPoint > list.Count Then
                      list.Add(converter(e.NewItems(i)))
                  Else
                      list.Insert(insertionPoint, converter(e.NewItems(i)))
                  End If
              Next
          End Sub
      
          Private Function ConvertFromMasterToTarget(ByVal masterListItem As Object) As Object
              Return If(_masterTargetConverter Is Nothing, masterListItem, _masterTargetConverter.Convert(masterListItem))
          End Function
      
          Private Function ConvertFromTargetToMaster(ByVal targetListItem As Object) As Object
              Return If(_masterTargetConverter Is Nothing, targetListItem, _masterTargetConverter.ConvertBack(targetListItem))
          End Function
      
          Private Sub HandleCollectionChanged(ByVal sender As Object, ByVal e As NotifyCollectionChangedEventArgs)
              Dim sourceList As IList = TryCast(sender, IList)
      
              Select Case e.Action
                  Case NotifyCollectionChangedAction.Add
                      PerformActionOnAllLists(AddressOf AddItems, sourceList, e)
                      Exit Select
                  Case NotifyCollectionChangedAction.Move
                      PerformActionOnAllLists(AddressOf MoveItems, sourceList, e)
                      Exit Select
                  Case NotifyCollectionChangedAction.Remove
                      PerformActionOnAllLists(AddressOf RemoveItems, sourceList, e)
                      Exit Select
                  Case NotifyCollectionChangedAction.Replace
                      PerformActionOnAllLists(AddressOf ReplaceItems, sourceList, e)
                      Exit Select
                  Case NotifyCollectionChangedAction.Reset
                      UpdateListsFromSource(TryCast(sender, IList))
                      Exit Select
                  Case Else
                      Exit Select
              End Select
          End Sub
      
          Private Sub MoveItems(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
              RemoveItems(list, e, converter)
              AddItems(list, e, converter)
          End Sub
      
          Private Sub PerformActionOnAllLists(ByVal action As ChangeListAction, ByVal sourceList As IList, ByVal collectionChangedArgs As NotifyCollectionChangedEventArgs)
              If sourceList Is _masterList Then
                  PerformActionOnList(_targetList, action, collectionChangedArgs, AddressOf ConvertFromMasterToTarget)
              Else
                  PerformActionOnList(_masterList, action, collectionChangedArgs, AddressOf ConvertFromTargetToMaster)
              End If
          End Sub
      
          Private Sub PerformActionOnList(ByVal list As IList, ByVal action As ChangeListAction, ByVal collectionChangedArgs As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
              StopListeningForChangeEvents(list)
              action(list, collectionChangedArgs, converter)
              ListenForChangeEvents(list)
          End Sub
      
          Private Sub RemoveItems(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
              Dim itemCount As Integer = e.OldItems.Count
      
              ' for the number of items being removed, remove the item from the Old Starting Index
              ' (this will cause following items to be shifted down to fill the hole).
              For i As Integer = 0 To itemCount - 1
                  list.RemoveAt(e.OldStartingIndex)
              Next
          End Sub
      
          Private Sub ReplaceItems(ByVal list As IList, ByVal e As NotifyCollectionChangedEventArgs, ByVal converter As Converter(Of Object, Object))
              RemoveItems(list, e, converter)
              AddItems(list, e, converter)
          End Sub
      
          Private Sub SetListValuesFromSource(ByVal sourceList As IList, ByVal targetList As IList, ByVal converter As Converter(Of Object, Object))
              StopListeningForChangeEvents(targetList)
      
              targetList.Clear()
      
              For Each o As Object In sourceList
                  targetList.Add(converter(o))
              Next
      
              ListenForChangeEvents(targetList)
          End Sub
      
          Private Function TargetAndMasterCollectionsAreEqual() As Boolean
              Return _masterList.Cast(Of Object)().SequenceEqual(_targetList.Cast(Of Object)().[Select](Function(item) ConvertFromTargetToMaster(item)))
          End Function
      
          ''' <summary>
          ''' Makes sure that all synchronized lists have the same values as the source list.
          ''' </summary>
          ''' <param name="sourceList">The source list.</param>
          Private Sub UpdateListsFromSource(ByVal sourceList As IList)
              If sourceList Is _masterList Then
                  SetListValuesFromSource(_masterList, _targetList, AddressOf ConvertFromMasterToTarget)
              Else
                  SetListValuesFromSource(_targetList, _masterList, AddressOf ConvertFromTargetToMaster)
              End If
          End Sub
      
      
      
      
          ''' <summary>
          ''' An implementation that does nothing in the conversions.
          ''' </summary>
          Friend Class DoNothingListItemConverter
              Implements IListItemConverter
      
              ''' <summary>
              ''' Converts the specified master list item.
              ''' </summary>
              ''' <param name="masterListItem">The master list item.</param>
              ''' <returns>The result of the conversion.</returns>
              Public Function Convert(ByVal masterListItem As Object) As Object Implements IListItemConverter.Convert
                  Return masterListItem
              End Function
      
              ''' <summary>
              ''' Converts the specified target list item.
              ''' </summary>
              ''' <param name="targetListItem">The target list item.</param>
              ''' <returns>The result of the conversion.</returns>
              Public Function ConvertBack(ByVal targetListItem As Object) As Object Implements IListItemConverter.ConvertBack
                  Return targetListItem
              End Function
          End Class
      
      End Class
      

      那么对于 XAML:

      <DataGrid ..... local:MultiSelectorBehaviours.SynchronizedSelectedItems="{Binding SelectedResults}" />
      

      最后是虚拟机:

      Public ReadOnly Property SelectedResults As ObservableCollection(Of StatisticsResultModel)
          Get
              Return _objSelectedResults
          End Get
      End Property
      

      贷方:http://blog.functionalfun.net/2009/02/how-to-databind-to-selecteditems.html

      【讨论】:

        【解决方案11】:

        此问题已在 WPF 问题队列中标记为“增强”:

        https://github.com/dotnet/wpf/issues/3140


        也许最终会进行修复/更新,并结合现成的解决方案。

        【讨论】:

          【解决方案12】:

          使用转换器怎么样?

          public class SelectedItemsConverter : IValueConverter
          {
              public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
              {
                  var dg = value as DataGrid;
                  return dg?.SelectedItems;
              }
          ...
          

          像这样在DataGridContextMenu 中使用它:

                 <DataGrid.ContextMenu>
                      <ContextMenu>
                          <MenuItem
                              CommandParameter="{Binding Path=MySelectedItems, Converter={StaticResource selectedItemsConverter}, Mode=OneWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"
                              Command="{Binding MyDoSomethingWithMySelectedItemsCommand}"
                              Header="Do Something...">
                          </MenuItem>
          

          【讨论】:

            【解决方案13】:

            devuxer提到的解决方案

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

            在拥有大型数据集时不起作用。您将需要禁用行虚拟化,而这正是您需要的情况......

            EnableRowVirtualization="False"

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2015-06-08
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-04-15
              • 2018-12-03
              • 1970-01-01
              相关资源
              最近更新 更多