WPF数据网格:将SelectedCells绑定到ViewModel的MVVM友好方式

7
我正在使用WPF数据网格,并且设置了SelectionUnit="Cell"和SelectionMode="Extended"。我也尽可能地遵循MVVM原则。
我需要我的ViewModel跟踪当前SelectedCells。
如果我可以将其SelectedCells属性绑定到我的ViewModel,那么生活就会变得容易。奇怪的是,只有在我们第一次选择网格中的任何单元格时,SelectedCells才会被触发一次。
MS在这里解释了它:http://social.msdn.microsoft.com/Forums/en/wpf/thread/737117f4-6d20-4232-88cf-e52cc44d4431 有人能想到一个符合MVVM的方法来解决这个问题吗?
谢谢!
4个回答

12

我意识到我的上一个回答是针对SelectedItems而不是SelectedCells,因此我编写了一个完整的附加属性类来为多个SelectedCells进行数据绑定,其工作方式如下:

<controls:DataGrid ItemsSource="{StaticResource list}"
                   SelectionMode="Extended"
                 behaviors:DataGridSelectedCellsBehavior.SelectedCells="{Binding Path=SelectedGridCellCollection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>  

我有一份工作的源代码和一个演示项目,在这里

附加属性行为代码:

public class DataGridSelectedCellsBehavior
{
    // Source : https://archive.codeplex.com/?p=datagridthemesfromsl
    // Credit to : T. Webster, https://stackoverflow.com/users/266457/t-webster

    public static IList<DataGridCellInfo> GetSelectedCells(DependencyObject obj)
    {
        return (IList<DataGridCellInfo>)obj.GetValue(SelectedCellsProperty);
    }
    public static void SetSelectedCells(DependencyObject obj, IList<DataGridCellInfo> value)
    {
        obj.SetValue(SelectedCellsProperty, value);
    }

    public static readonly DependencyProperty SelectedCellsProperty = DependencyProperty.RegisterAttached("SelectedCells", typeof(IList<DataGridCellInfo>), typeof(DataGridSelectedCellsBehavior), new UIPropertyMetadata(null, OnSelectedCellsChanged));

    static SelectedCellsChangedEventHandler GetSelectionChangedHandler(DependencyObject obj)
    {
        return (SelectedCellsChangedEventHandler)obj.GetValue(SelectionChangedHandlerProperty);
    }
    static void SetSelectionChangedHandler(DependencyObject obj, SelectedCellsChangedEventHandler value)
    {
        obj.SetValue(SelectionChangedHandlerProperty, value);
    }

    static readonly DependencyProperty SelectionChangedHandlerProperty = DependencyProperty.RegisterAttached("SelectedCellsChangedEventHandler", typeof(SelectedCellsChangedEventHandler), typeof(DataGridSelectedCellsBehavior), new UIPropertyMetadata(null));

    //d is MultiSelector (d as ListBox not supported)
    static void OnSelectedCellsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
    {
        if (GetSelectionChangedHandler(d) != null)
            return;

        if (d is DataGrid)//DataGrid
        {
            DataGrid datagrid = d as DataGrid;
            SelectedCellsChangedEventHandler selectionchanged = null;
            foreach (var selected in GetSelectedCells(d) as IList<DataGridCellInfo>)
                datagrid.SelectedCells.Add(selected);

            selectionchanged = (sender, e) =>
            {
                SetSelectedCells(d, datagrid.SelectedCells);
            };
            SetSelectionChangedHandler(d, selectionchanged);
            datagrid.SelectedCellsChanged += GetSelectionChangedHandler(d);
        }
        //else if (d is ListBox)
        //{
        //    ListBox listbox = d as ListBox;
        //    SelectionChangedEventHandler selectionchanged = null;

        //    selectionchanged = (sender, e) =>
        //    {
        //        SetSelectedCells(d, listbox.SelectedCells);
        //    };
        //    SetSelectionChangedHandler(d, selectionchanged);
        //    listbox.SelectionChanged += GetSelectionChangedHandler(d);
        //}
    }

}

视图模型代码:

class DemoViewModel : INotifyPropertyChanged
{  
    private IList<DataGridCellInfo> selectedGridCellCollection = new List<DataGridCellInfo>();
    public IList<DataGridCellInfo> SelectedGridCellCollection
    {
        get { return selectedGridCellCollection; }
        set
        {
            selectedGridCellCollection = value;
            NotifyPropertyChanged();
        }
    }

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

如果你使用的是.NET4,T.Webster的回答很好。显然,如果你使用的是.NET3.5并且从WPF工具包中获取DataGrid,则可能没有简单的答案,你将不得不捕获SelectedCellsChanged事件,并手动获取'myGrid.SelectedCells'并将其传递给你的ViewModel。我知道这听起来很糟糕,但可能没有其他办法... - GuYsH
我检查了你的网格,它看起来很不错,但如果我们有一个复选框可以同时选择/取消所有列,那么它会非常缓慢。 - Quantbuff

2
您需要被选定的单元格(SelectedCells)是否需要实时数据绑定,或者只是在用户点击“OK/Accept”按钮时才需要?如果您仅需要在用户完成某个过程时获取SelectedCells,则可以将SelectedCells绑定到Button的CommandParameter属性上。SelectedCells是一个IList类型,您可以通过将其强制转换为所选对象类型来获取足够的信息。另一种选择比较麻烦,您可以使用附加属性来处理事件,使其不需要出现在Views中。这个附加属性可以处理ListBox或者在您的情况下是DataGrid(MultiSelector)。
public class Attach 
{
    public static IList GetSelectedItems(DependencyObject obj)
    {
        return (IList)obj.GetValue(SelectedItemsProperty);
    }

    public static void SetSelectedItems(DependencyObject obj, IList value)
    {
        obj.SetValue(SelectedItemsProperty, value);
    }

    /// <summary>
    /// Attach this property to expose the read-only SelectedItems property of a MultiSelector for data binding.
    /// </summary>
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.RegisterAttached("SelectedItems", typeof(IList), typeof(Attach), new UIPropertyMetadata(new List<object>() as IList, OnSelectedItemsChanged));



    static SelectionChangedEventHandler GetSelectionChangedHandler(DependencyObject obj)
    {
        return (SelectionChangedEventHandler)obj.GetValue(SelectionChangedHandlerProperty);
    }
    static void SetSelectionChangedHandler(DependencyObject obj, SelectionChangedEventHandler value)
    {
        obj.SetValue(SelectionChangedHandlerProperty, value);
    }
    static readonly DependencyProperty SelectionChangedHandlerProperty =
        DependencyProperty.RegisterAttached("SelectionChangedHandler", typeof(SelectionChangedEventHandler), typeof(Attach), new UIPropertyMetadata(null));


    //d is MultiSelector (d as ListBox not supported)
    static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
    {
        if (GetSelectionChangedHandler(d) != null)
            return;

        if (d is MultiSelector)//DataGrid
        {
            MultiSelector multiselector = d as MultiSelector;
            SelectionChangedEventHandler selectionchanged = null;
            foreach (var selected in GetSelectedItems(d) as IList)
                multiselector.SelectedItems.Add(selected);

            selectionchanged = (sender, e) =>
            {
                SetSelectedItems(d, multiselector.SelectedItems);
            };
            SetSelectionChangedHandler(d, selectionchanged);
            multiselector.SelectionChanged += GetSelectionChangedHandler(d);
        }
        else if (d is ListBox)
        {
            ListBox listbox = d as ListBox;
            SelectionChangedEventHandler selectionchanged = null;

            selectionchanged = (sender, e) =>
            {
                SetSelectedItems(d, listbox.SelectedItems);
            };
            SetSelectionChangedHandler(d, selectionchanged);
            listbox.SelectionChanged += GetSelectionChangedHandler(d);
        }}}

XAML中的用法:

<DataGrid ItemsSource="{Binding Path=SourceList}"
              myControls:Attach.SelectedItems="{Binding Path=myMvvmSelectedItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
              SelectionMode="Extended" />

1

你可能会对WPF应用程序框架(WAF)BookLibrary示例应用程序感兴趣。它展示了如何将DataGrid.SelectedItems与ViewModel同步。这可能与SelectedCells非常相似。


1
只是为其他读者添加一些小信息:该示例基于代码后台事件处理程序,这些处理程序在“SelectionChangedEvent”中更新ViewModel,而不是在ViewModel属性和“DataGrid.SelectedItems”属性之间进行数据绑定。尽管如此,如果有人不想遵循过于严格的MVVM规则,它仍然是有用的。 - Slauma
Model-View-ViewModel(MVVM)是一种设计模式。设计模式描述了对象如何允许交互。但是它们不会禁止像代码后台这样的方面。如果你查看绑定或行为内部的工作方式,那么你会发现它们也调用 ViewModel 或 Model 上的方法。这并不违反 MVVM。 - jbe

0
在完美的MVVM绑定和完整的事件处理程序代码之间,有一片灰色区域叫做交互式EventTriggers(请参见Blend SDK) :)
如果您将一个EventTrigger放置到数据表格中,并将其设置为“SelectionChanged”,并将eventargs传递给一个命令(使用EventToCommand actiontrigger),您就可以从eventargs中获取所选项目了...
或者像MS线程中说的那样使用Multibinding :)

TDaver:听起来是个好主意,但奇怪的是,当 DataGrid 设置为 SelectionMode=Cell 时,当选中单元格时 SelectionChanges 事件不被触发,而在 SelectionMode="FullRow" 时却可以对整行触发。这很奇怪,但却是事实... - GuYsH
SelectionChanges 还是 SelectionChanged? - TDaver
还有一个问题...你需要VM中包含的CELLS还是ITEMS?前者不太适合MVVM,但前者只需要SelectedItems属性而不是SelectedCells,我认为后者会在选择不同行中的单元格时被触发(因为如果单元格在同一行,则它们是相同的项...) - TDaver
我使用了这种方法,并且在 mvvm light 中运作得非常好,而且它也是最快的方式。无缝选择和删除了10000行! - Quantbuff

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