wpfpropertychangedeventsetter

Can you have an event setter bound to an event of the data context?


Im trying to create something like this -

enter image description here

I have an observable collection of points. Each point has a position and a colour. When any points position or colour changes(they implement notification change), I want to "repaint" the background gradient. Currently I have an itemscontrol where I have the sliders bound to the points position and the gradient is initially drawn. Now, I want to know how I can call a function in the code behind of my view when the propertychanged event on a 'point' fires, so that I can repaint the gradient. Im wondering if an event setter can somehow be used?

Whilst I could do the propertychanged event subscribing in code behind, I'd like to do it in XAML?

PLease note : I specifically want to take this approach of manually repainting in code behind for other reasons, so if I could get answers to the specific problem above rather than alternative solutions please.


Solution

  • I guess you can create an attached property to subscribe to PropertyChanged events of the value of the DataContext property.

    public static class Props
    {
        public static DependencyProperty OnPropertyChangedProperty = DependencyProperty.RegisterAttached(
            "OnPropertyChanged", typeof(PropertyChangedEventHandler), typeof(Props),
            new PropertyMetadata(OnPropertyChangedPropertyChanged));
    
        public static PropertyChangedEventHandler GetOnPropertyChanged (DependencyObject d)
        {
            return (PropertyChangedEventHandler)d.GetValue(OnPropertyChangedProperty);
        }
    
        public static void SetOnPropertyChanged (DependencyObject d, PropertyChangedEventHandler value)
        {
            d.SetValue(OnPropertyChangedProperty, value);
        }
    
        private static void OnPropertyChangedPropertyChanged (DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var inpc = (INotifyPropertyChanged)((FrameworkElement)d).DataContext;
            if (inpc == null)
                throw new ArgumentException("DataContext of the framework element must not be null.");
            var oldChanged = (PropertyChangedEventHandler)e.OldValue;
            if (oldChanged != null)
                inpc.PropertyChanged -= oldChanged;
            var newChanged = (PropertyChangedEventHandler)e.NewValue;
            if (newChanged != null)
                inpc.PropertyChanged += newChanged;
        }
    }
    

    Usage:

    <Window x:Class="So17382721PropertyChangedXaml.MainWindow" x:Name="root"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:So17382721PropertyChangedXaml"
            Title="MainWindow" Height="350" Width="525">
        <Window.Resources>
            <DataTemplate DataType="{x:Type local:Foo}">
                <!-- Here, we subscribe to DataContext.PropertyChanged;
                     handler is defined in the MainWindow class -->
                <Grid local:Props.OnPropertyChanged="{Binding FooPropertyChanged, ElementName=root}">
                    <TextBox Text="{Binding Bar, UpdateSourceTrigger=PropertyChanged}"/>
                </Grid>
            </DataTemplate>
        </Window.Resources>
        <Grid>
            <ItemsControl ItemsSource="{Binding Foos, ElementName=root}"/>
        </Grid>
    </Window>
    

    Code-behind:

    using System;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    using System.Windows;
    
    namespace So17382721PropertyChangedXaml
    {
        public partial class MainWindow
        {
            public ObservableCollection<Foo> Foos { get; private set; }
    
            public MainWindow ()
            {
                Foos = new ObservableCollection<Foo> {
                    new Foo { Bar = "1" },
                    new Foo { Bar = "2" },
                    new Foo { Bar = "3" },
                };
                InitializeComponent();
            }
    
            private void OnFooPropertyChanged (object sender, PropertyChangedEventArgs e)
            {
                MessageBox.Show(this, string.Format("{0} of {1} changed.", e.PropertyName, sender));
            }
    
            // Subscribing to non-RoutedEvents in XAML is not straightforward, but we can define a property
            public PropertyChangedEventHandler FooPropertyChanged
            {
                get { return OnFooPropertyChanged; }
            }
        }
    
        public class Foo : INotifyPropertyChanged
        {
            private string _bar;
            public string Bar
            {
                get { return _bar; }
                set
                {
                    _bar = value;
                    OnPropertyChanged();
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            protected virtual void OnPropertyChanged ([CallerMemberName] string propertyName = null)
            {
                var handler = PropertyChanged;
                if (handler != null)
                    handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    

    Note: the attached property Props.OnPropertyChanged expects that DataContext is not changed during lifetime and is already specified. Handling DataContextChanged events is left as an exircize, if you need it.