WPF Datagrid: MVVM friendly way to bind SelectedCe

2019-01-28 02:22发布

问题:

I'm using the WPF datagrid, and have SelectionUnit="Cell" and SelectionMode="Extended". I'm also trying to adhere to the MVVM principals as much as I can.

I need my ViewModel to keep track of the current SelectedCells.

Life would be easy if I could just Bind its SelectedCells property to my ViewModel. Oddly enough, SelectedCells is only raised once - when we first select any cell in the grid.

MS explains it here: http://social.msdn.microsoft.com/Forums/en/wpf/thread/737117f4-6d20-4232-88cf-e52cc44d4431

Can anyone think of an MVVM-friendly approach to get around it?

Thanks!

回答1:

I realized my last answer was for SelectedItems instead of SelectedCells, so I wrote a complete attached property class to do data binding for multiple SelectedCells which works as follows:

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

I have a working source code and a demo project of it here.

Attached property behavior Code :

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

}

View Model Code :

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


回答2:

Do you need the SelectedCells constantly data-binded, or just when the user hits the OK/Accept button? If you only need it at the end of whatever process the user is in you can bind the SelectedCells to the CommandParameter property of a Button, for example. The SelectedCells is an IList, and you know enough to just do a cast to whatever object type the selection actually is. The other option is messier, you can use an attached property, keeping the event-handling out of your Views. This attached property would handle either a ListBox or in your case a 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);
        }}}

Usage in XAML:

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


回答3:

You might be interested in the BookLibrary sample application of the WPF Application Framework (WAF). It shows how to synchronize the DataGrid.SelectedItems with the ViewModel. This might be very similar to SelectedCells.



回答4:

Somewhere between perfect MVVM bindings and full event handler codebehinds there is the gray area of interactivity EventTriggers (see Blend SDK) :)
If you put an eventtrigger to the datagrid, and set to "SelectionChanged" and pass the eventargs to a command (use an EventToCommand actiontrigger) you could get the selected items from the eventargs hopefully...
Or use the multibinding as said in the MS thread :)