How to support ListBox SelectedItems binding with MVVM in a navigable application

前端 未结 9 1179
有刺的猬
有刺的猬 2020-11-27 15:06

I am making a WPF application that is navigable via custom \"Next\" and \"Back\" buttons and commands (i.e. not using a NavigationWindow). On one screen, I have

相关标签:
9条回答
  • 2020-11-27 15:45

    This was a major issue for me, some of the answers I have seen were either too hackish, or required resetting the SelectedItems property value breaking any code attached to the properties OnCollectionChanged event. But I managed to get a workable solution by modifying the collection directly and as a bonus it even supports SelectedValuePath for object collections.

    public class MultipleSelectionListBox : ListBox
    {
        internal bool processSelectionChanges = false;
    
        public static readonly DependencyProperty BindableSelectedItemsProperty =
            DependencyProperty.Register("BindableSelectedItems",
                typeof(object), typeof(MultipleSelectionListBox),
                new FrameworkPropertyMetadata(default(ICollection<object>),
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
    
        public dynamic BindableSelectedItems
        {
            get => GetValue(BindableSelectedItemsProperty);
            set => SetValue(BindableSelectedItemsProperty, value);
        }
    
    
        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
    
            if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls
    
            if (e.AddedItems.Count > 0)
                if (!string.IsNullOrWhiteSpace(SelectedValuePath))
                {
                    foreach (var item in e.AddedItems)
                        if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                            BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
                }
                else
                {
                    foreach (var item in e.AddedItems)
                        if (!BindableSelectedItems.Contains((dynamic)item))
                            BindableSelectedItems.Add((dynamic)item);
                }
    
            if (e.RemovedItems.Count > 0)
                if (!string.IsNullOrWhiteSpace(SelectedValuePath))
                {
                    foreach (var item in e.RemovedItems)
                        if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                            BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
                }
                else
                {
                    foreach (var item in e.RemovedItems)
                        if (BindableSelectedItems.Contains((dynamic)item))
                            BindableSelectedItems.Remove((dynamic)item);
                }
        }
    
        private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is MultipleSelectionListBox listBox)
            {
                List<dynamic> newSelection = new List<dynamic>();
                if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
                    foreach (var item in listBox.BindableSelectedItems)
                    {
                        foreach (var lbItem in listBox.Items)
                        {
                            var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
                            if ((dynamic)lbItemValue == (dynamic)item)
                                newSelection.Add(lbItem);
                        }
                    }
                else
                    newSelection = listBox.BindableSelectedItems as List<dynamic>;
    
                listBox.SetSelectedItems(newSelection);
            }
        }
    }
    

    Binding works just as you would have expected MS to have done themselves:

    <uc:MultipleSelectionListBox 
        ItemsSource="{Binding Items}" 
        SelectionMode="Extended" 
        SelectedValuePath="id" 
        BindableSelectedItems="{Binding mySelection}"
    />
    

    It has not been thoroughly tested but has passed first glance inspections. I tried to keep it reuseable by employing dynamic types on the collections.

    0 讨论(0)
  • 2020-11-27 15:47

    Here's yet another solution. It's similar to Ben's answer, but the binding works two ways. The trick is to update the ListBox's selected items when the bound data items change.

    public class MultipleSelectionListBox : ListBox
    {
        public static readonly DependencyProperty BindableSelectedItemsProperty =
            DependencyProperty.Register("BindableSelectedItems",
                typeof(IEnumerable<string>), typeof(MultipleSelectionListBox),
                new FrameworkPropertyMetadata(default(IEnumerable<string>),
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));
    
        public IEnumerable<string> BindableSelectedItems
        {
            get => (IEnumerable<string>)GetValue(BindableSelectedItemsProperty);
            set => SetValue(BindableSelectedItemsProperty, value);
        }
    
        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            BindableSelectedItems = SelectedItems.Cast<string>();
        }
    
        private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is MultipleSelectionListBox listBox)
                listBox.SetSelectedItems(listBox.BindableSelectedItems);
        }
    }
    

    Unfortunately, I wasn't able to use IList as the BindableSelectedItems type. Doing so sent null to my view model's property, whose type is IEnumerable<string>.

    Here's the XAML:

    <v:MultipleSelectionListBox
        ItemsSource="{Binding AllMyItems}"
        BindableSelectedItems="{Binding MySelectedItems}"
        SelectionMode="Multiple"
        />
    

    There's one thing to watch out for. In my case, a ListBox may be removed from the view. For some reason, this causes the SelectedItems property to change to an empty list. This, in turn, causes the view model's property to be changed to an empty list. Depending on your use case, this may not be desirable.

    0 讨论(0)
  • Rachel's solutions works great! But there is one problem I've encountered - if you override the style of ListBoxItem, you loose the original styling applied to it (in my case responsible for highlighting the selected item etc.). You can avoid this by inheriting from the original style:

    <Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
        <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
    </Style>
    

    Note setting BasedOn (see this answer) .

    0 讨论(0)
  • 2020-11-27 15:50

    I couldn't get Rachel's solution to work how I wanted it, but I found Sandesh's answer of creating a custom dependency property to work perfectly for me. I just had to write similar code for a ListBox:

    public class ListBoxCustom : ListBox
    {
        public ListBoxCustom()
        {
            SelectionChanged += ListBoxCustom_SelectionChanged;
        }
    
        void ListBoxCustom_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            SelectedItemsList = SelectedItems;
        }
    
        public IList SelectedItemsList
        {
            get { return (IList)GetValue(SelectedItemsListProperty); }
            set { SetValue(SelectedItemsListProperty, value); }
        }
    
        public static readonly DependencyProperty SelectedItemsListProperty =
           DependencyProperty.Register(nameof(SelectedItemsList), typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));
    
    }
    

    In my View Model I just referenced that property to get my selected list.

    0 讨论(0)
  • 2020-11-27 15:53

    Not satisfied with the given answers I was trying to find one by myself... Well it turns out to be more like a hack then a solution but for me that works fine. This Solution uses MultiBindings in a special way. First it may look like a ton of Code but you can reuse it with very little effort.

    First I implemented a 'IMultiValueConverter'

    public class SelectedItemsMerger : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            SelectedItemsContainer sic = values[1] as SelectedItemsContainer;
    
            if (sic != null)
                sic.SelectedItems = values[0];
    
            return values[0];
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return new[] { value };
        }
    }
    

    And a SelectedItems Container/Wrapper:

    public class SelectedItemsContainer
    {
        /// Nothing special here...
        public object SelectedItems { get; set; }
    }
    

    Now we create the Binding for our ListBox.SelectedItem (Singular). Note: You have to create a static Resource for the 'Converter'. This may be done once per application and be reused for all ListBoxes that need the converter.

    <ListBox.SelectedItem>
     <MultiBinding Converter="{StaticResource SelectedItemsMerger}">
      <Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" Path="SelectedItems"/>
      <Binding Path="SelectionContainer"/>
     </MultiBinding>
    </ListBox.SelectedItem>
    

    In the ViewModel I created the Container where I can bind to. It is important to initialize it with new() in order to fill it with the values.

        SelectedItemsContainer selectionContainer = new SelectedItemsContainer();
        public SelectedItemsContainer SelectionContainer
        {
            get { return this.selectionContainer; }
            set
            {
                if (this.selectionContainer != value)
                {
                    this.selectionContainer = value;
                    this.OnPropertyChanged("SelectionContainer");
                }
            }
        }
    

    And that's it. Maybe someone sees some improvements? What do You think about it?

    0 讨论(0)
  • 2020-11-27 15:54

    This was pretty easy to do with a Command and the Interactivities EventTrigger. ItemsCount is just a bound property to use on your XAML, should you want to display the updated count.

    XAML:

         <ListBox ItemsSource="{Binding SomeItemsSource}"
                     SelectionMode="Multiple">
            <i:Interaction.Triggers>
             <i:EventTrigger EventName="SelectionChanged">
                <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" 
                                       CommandParameter="{Binding ElementName=MyView, Path=SelectedItems.Count}" />
             </i:EventTrigger>
            </Interaction.Triggers>    
        </ListView>
    
    <Label Content="{Binding ItemsCount}" />
    

    ViewModel:

        private int _itemsCount;
        private RelayCommand<int> _selectionChangedCommand;
    
        public ICommand SelectionChangedCommand
        {
           get {
                    return _selectionChangedCommand ?? (_selectionChangedCommand = 
                 new RelayCommand<int>((itemsCount) => { ItemsCount = itemsCount; }));
               }
        }
    
            public int ItemsCount
            {
                get { return _itemsCount; }
                set { 
                  _itemsCount = value;
                  OnPropertyChanged("ItemsCount");
                 }
            }
    
    0 讨论(0)
提交回复
热议问题