Search code examples
c#wpfxamllistboxattached-properties

Why is this attached property not updating?


Alright, so I was originally just going to post this as an issue of why the binding was not working. However, as soon as I tried to minimise the code to post on SO it created yet another issue.

So, essentially, the Extensions class is meant to hook onto the ListBox and make a bindable version of SelectedItems. This feature of grabbing the SelectedItems and placing them in the Selected attached property works in my program (in my real program it does not seem to bind?), but not in this minimised version. I have no clue why however, the code seems to do all it needs to do.

The code I used to test it:

.xaml.cs

namespace MyNamespace
{
    public partial class MainWindow
    {
        public IList Selected { get; set; }
        public MainWindow()
        {
            InitializeComponent();
            Selected = new List<object>();
            Why.ItemsSource = new[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        }
        private void Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(Selected.Cast<object>().Aggregate("", (s, info) => $"{s}{info}, ").TrimEnd(',', ' '));
            MessageBox.Show(Extensions.GetSelected(Why).Cast<object>().Aggregate("", (s, o) => $"{s}{o}, ").TrimEnd(',', ' '));
        }
    }
    public static class Extensions
    {
        public static readonly DependencyProperty SelectedProperty = DependencyProperty.RegisterAttached(
            "Selected", typeof(IList), typeof(Extensions), new PropertyMetadata(default(IList), HookSelectionChanged));

        private static void HookSelectionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ListBox lb = sender as ListBox;
            if (lb == null)
                throw new ArgumentException("This property currently only supports DependencyObjects inheriting from Selector.", nameof(sender));
            lb.SelectionChanged += SelectionChanged;
        }

        private static void SelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
            => SetSelected((ListBox)sender, ((ListBox)sender).SelectedItems.Cast<object>().ToList());

        public static void SetSelected(DependencyObject element, IList value) => element.SetValue(SelectedProperty, value);

        public static IList GetSelected(DependencyObject element) => (IList)element.GetValue(SelectedProperty);
    }
}

.xaml

<Window x:Class="MyNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:lcl="clr-namespace:MyNamespace"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="My title." Height="350" Width="425" MaxHeight="350" MaxWidth="425" MinHeight="350" MinWidth="425">
    <StackPanel>
        <ListBox lcl:Extensions.Selected="{Binding Selected}" x:Name="Why" SelectionMode="Extended"/>
        <Button Click="Click" Content="blah"/>
    </StackPanel>
</Window>

Any ideas would be excellent! Thanks :)


Solution

  • Alright, so all it was was me missing two things:

    • I didn't implement INotifyPropertyChanged in my test
    • I did not know that Bindings did not default to TwoWay all of the time.

    So all that needed to be done was this:

    .xaml.cs

    namespace MyNamespace
    {
        public partial class MainWindow : INotifyPropertyChanged
        {
            private IList _selected;
    
            public IList Selected
            {
                get { return _selected; }
                set
                {
                    if (Equals(value, _selected)) return;
                    _selected = value;
                    OnPropertyChanged();
                }
            }
    
            public MainWindow()
            {
                InitializeComponent();
                Selected = new List<object>();
                Why.ItemsSource = new[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
            }
            private void Click(object sender, RoutedEventArgs e)
            {
                MessageBox.Show(Selected.Cast<object>().Aggregate("", (s, info) => $"{s}{info}, ").TrimEnd(',', ' '));
                MessageBox.Show(Extensions.GetSelected(Why).Cast<object>().Aggregate("", (s, o) => $"{s}{o}, ").TrimEnd(',', ' '));
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            [NotifyPropertyChangedInvocator]
            protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        public class Extensions
        {
            public static readonly DependencyProperty SelectedProperty = DependencyProperty.RegisterAttached(
                "Selected", typeof(IList), typeof(Extensions), new FrameworkPropertyMetadata(default(IList), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, HookSelectionChanged));
    
            private static void HookSelectionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
            {
                ListBox lb = sender as ListBox;
                if (lb == null)
                    throw new ArgumentException("This property currently only supports DependencyObjects inheriting from Selector.", nameof(sender));
                lb.SelectionChanged += SelectionChanged;
            }
    
            private static void SelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
                => SetSelected((ListBox)sender, ((ListBox)sender).SelectedItems.Cast<object>().ToList());
    
            public static void SetSelected(DependencyObject element, IList value) => element.SetValue(SelectedProperty, value);
    
            public static IList GetSelected(DependencyObject element) => (IList)element.GetValue(SelectedProperty);
        }
    }
    

    and it worked brilliantly.