Bind to SelectedItems from DataGrid or ListBox in

2019-01-17 12:21发布

SEE MY ANSWER AT THE BOTTOM

Just doing some light reading on WPF where I need to bind the selectedItems from a DataGrid but I am unable to come up with anything tangible. I just need the selected objects.

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

10条回答
成全新的幸福
2楼-- · 2019-01-17 12:49

With a bit of trickery you can extend the DataGrid to create a bindable version of the SelectedItems property. My solution requires the binding to have Mode=OneWayToSource since I only want to read from the property anyway, but it might be possible to extend my solution to allow the property to be read-write.

I assume a similar technique could be used for ListBox, but I haven't tried it.

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);
    }
}
查看更多
祖国的老花朵
3楼-- · 2019-01-17 12:51

I came here for the answer and I got a lot of great ones. I combined them all into an attached property, very similar to the one offered by Omar above, but in one class. Handles INotifyCollectionChanged and switching the list out. Doesn't leak events. I wrote it so it would be pretty simple code to follow. Written in C#, handles listbox selectedItems and dataGrid selectedItems.

This works on both DataGrid and ListBox.

(I just learned how to use GitHub) GitHub https://github.com/ParrhesiaJoe/SelectedItemsAttachedWpf

To Use:

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

And here's the code. There is a little sample on 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;
        }
    }
 }
查看更多
在下西门庆
4楼-- · 2019-01-17 12:54

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:

  1. Following Ed Ball's suggestion', on you XAML command databinding, define CommandParameter property BEFORE Command property. This a very time-consuming bug.

    enter image description here

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

查看更多
Rolldiameter
5楼-- · 2019-01-17 13:00

The solution mentioned by devuxer

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

does not work when having large dataset. You will need to disable row virtualization, and that is exactly the case where you need it...

EnableRowVirtualization="False"

查看更多
来,给爷笑一个
6楼-- · 2019-01-17 13:03

I know this post is a little old and has been answered. But I came up with a non MVVM answer. Its easy and works for me. Add another DataGrid, assuming your Selected Collection is SelectedResults:

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

And in code behind, add this to the Constructor:

    public ClassConstructor()
    {
        InitializeComponent();
        OriginalGrid.SelectionChanged -= OriginalGrid_SelectionChanged;
        OriginalGrid.SelectionChanged += OriginalGrid_SelectionChanged;
    }
    private void OriginalGrid_SelectionChanged(object sender, 
        SelectionChangedEventArgs e)
    {
        SelectedGridRows.ItemsSource = OriginalGrid.SelectedItems;
    }
查看更多
萌系小妹纸
7楼-- · 2019-01-17 13:04

I had a solution for this using a workaround that fited my needs.

Make an event to command on the listitemtemplate on MouseUp and as a commandParameter send the SelectedItems collection

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

That way you can have a command in the viewmodel that handles this or saves the selected items for later use. Have fun coding

查看更多
登录 后发表回答