Search code examples
c#wpfdata-bindingdependency-propertiesbehavior

Behavior DependencyProperty not updating ViewModel when used within DataTemplate


I have a DependencyProperty in a Behavior which I am setting the value for in OnAttached().

I am then binding view model properties to this DependencyProperty with a Mode of OneWayToSource.

For some reason the bound view model property does not get updated by the OneWayToSource binding when done within a DataTemplate (the view model's setter is never invoked). In other circumstances it appears to work fine.

I'm getting no binding errors and I can see no indications of any exceptions, etc, and am at a loss as to what it is I am doing wrong.

The WPF Designer does show some errors, claiming either The member "TestPropertyValue" is not recognized or is not accessible or The property "TestPropertyValue was not found in type 'TestBehavior', depending on where you look. I am unsure if these are 'real' errors (as I've observed the WPF Designer does not seem to be entirely reliable in always showing genuine issues), and if they are, are whether they are related to this issue or another problem entirely.

If these Designer errors do relate to this issue I can only assume that I must have declared the DependencyProperty incorrectly. If that is the case I am unable to see where the mistakes are.

I have produced an example project that replicates the issue. The following code should suffice and can be added to any new WPF project with the name WpfBehaviorDependencyPropertyIssue001.

MainWindow.xaml
<Window x:Class="WpfBehaviorDependencyPropertyIssue001.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:tb="clr-namespace:WpfBehaviorDependencyPropertyIssue001.Behaviors"
        xmlns:vm="clr-namespace:WpfBehaviorDependencyPropertyIssue001.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>
    <StackPanel>
        <Label Content="{Binding TestPropertyValue, ElementName=OuterTestA}" Background="Cyan">
            <b:Interaction.Behaviors>
                <tb:TestBehavior x:Name="OuterTestA" TestPropertyValue="{Binding MainTestValueA, Mode=OneWayToSource}" />
            </b:Interaction.Behaviors>
        </Label>
        <Label Content="{Binding MainTestValueA, Mode=OneWay}" Background="Orange" />
        <Label Content="{Binding MainTestValueB, Mode=OneWay}" Background="MediumPurple" />
        <DataGrid ItemsSource="{Binding Items}" RowDetailsVisibilityMode="Visible">
            <b:Interaction.Behaviors>
                <tb:TestBehavior x:Name="OuterTestB" TestPropertyValue="{Binding MainTestValueB, Mode=OneWayToSource}" />
            </b:Interaction.Behaviors>
            <DataGrid.RowDetailsTemplate>
                <DataTemplate>
                    <StackPanel>
                        <Label Content="{Binding TestPropertyValue, ElementName=InnerTest}" Background="Cyan">
                            <b:Interaction.Behaviors>
                                <tb:TestBehavior x:Name="InnerTest" TestPropertyValue="{Binding ItemTestViewModelValue, Mode=OneWayToSource}" />
                            </b:Interaction.Behaviors>
                        </Label>
                        <Label Content="{Binding ItemTestViewModelValue, Mode=OneWay}" Background="Lime" />
                    </StackPanel>
                </DataTemplate>
            </DataGrid.RowDetailsTemplate>
        </DataGrid>
    </StackPanel>
</Window>
TestBehavior.cs
using Microsoft.Xaml.Behaviors;
using System.Windows;

namespace WpfBehaviorDependencyPropertyIssue001.Behaviors
{
    public class TestBehavior : Behavior<UIElement>
    {
        public static DependencyProperty TestPropertyValueProperty { get; } = DependencyProperty.Register("TestPropertyValue", typeof(string), typeof(TestBehavior));

        // Remember, these two are just for the XAML designer (or I guess if we manually invoked them for some reason).
        public static string GetTestPropertyValue(DependencyObject dependencyObject) => (string)dependencyObject.GetValue(TestPropertyValueProperty);
        public static void SetTestPropertyValue(DependencyObject dependencyObject, string value) => dependencyObject.SetValue(TestPropertyValueProperty, value);

        protected override void OnAttached()
        {
            base.OnAttached();
            SetValue(TestPropertyValueProperty, "Example");
        }
    }
}
ViewModelBase.cs
using System.ComponentModel;

namespace WpfBehaviorDependencyPropertyIssue001.ViewModels
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
MainViewModel.cs
using System.Collections.ObjectModel;

namespace WpfBehaviorDependencyPropertyIssue001.ViewModels
{
    public class MainViewModel : ViewModelBase
    {
        public ObservableCollection<ItemViewModel> Items
        {
            get => _Items;
            set
            {
                _Items = value;
                OnPropertyChanged(nameof(Items));
            }
        }
        private ObservableCollection<ItemViewModel> _Items;

        public MainViewModel()
        {
            Items = new ObservableCollection<ItemViewModel>()
            {
                new ItemViewModel() { ItemName="Item 1" }
            };
        }

        public string MainTestValueA
        {
            get => _MainTestValueA;
            set
            {
                System.Diagnostics.Debug.WriteLine($"Setting {nameof(MainTestValueA)} to {(value != null ? $"\"{value}\"" : "null")}");
                _MainTestValueA = value;
                OnPropertyChanged(nameof(MainTestValueA));
            }
        }
        private string _MainTestValueA;

        public string MainTestValueB
        {
            get => _MainTestValueB;
            set
            {
                System.Diagnostics.Debug.WriteLine($"Setting {nameof(MainTestValueB)} to {(value != null ? $"\"{value}\"" : "null")}");
                _MainTestValueB = value;
                OnPropertyChanged(nameof(MainTestValueB));
            }
        }
        private string _MainTestValueB;
    }
}
ItemViewModel.cs
namespace WpfBehaviorDependencyPropertyIssue001.ViewModels
{
    public class ItemViewModel : ViewModelBase
    {
        public string ItemName
        {
            get => _ItemName;
            set
            {
                _ItemName = value;
                OnPropertyChanged(nameof(ItemName));
            }
        }
        private string _ItemName;

        public string ItemTestViewModelValue
        {
            get => _ItemTestViewModelValue;
            set
            {
                System.Diagnostics.Debug.WriteLine($"Setting {nameof(ItemTestViewModelValue)} to {(value != null ? $"\"{value}\"" : "null")}");
                _ItemTestViewModelValue = value;
                OnPropertyChanged(nameof(ItemTestViewModelValue));
            }
        }
        private string _ItemTestViewModelValue;
    }
}

Expected Debug output messages (excluding the standard WPF ones):

Setting MainTestValueA to null
Setting MainTestValueA to "Example"
Setting MainTestValueB to null
Setting MainTestValueB to "Example"
Setting ItemTestViewModelValue to null
Setting ItemTestViewModelValue to "Example"

Actual Debug output messages (excluding the standard WPF ones):

Setting MainTestValueA to null
Setting MainTestValueA to "Example"
Setting MainTestValueB to null
Setting MainTestValueB to "Example"
Setting ItemTestViewModelValue to null

Solution

  • I have managed to resolve the issue.

    For some reason, it looks like an UpdateSourceTrigger of PropertyChanged is needed for a binding within a DataTemplate that has a Mode of OneWayToSource. Doing this results in the view model property being updated correctly.

    I discovered this through experimentation, and I am uncertain why this behaviour differs from the binding done outside the DataTemplate, although it is possible that this behaviour is documented somewhere.

    If I can find the reason for this behaviour (documented or not) I will update this answer with that information.

    Additional information

    For clarity for future readers, the label with the OneWayToSource binding outside of the DataTemplate worked as expected. The XAML for this (from the original question) is shown below:

            <Label Content="{Binding TestPropertyValue, ElementName=OuterTestA}" Background="Cyan">
                <b:Interaction.Behaviors>
                    <tb:TestBehavior x:Name="OuterTestA" TestPropertyValue="{Binding MainTestValueA, Mode=OneWayToSource}" />
                </b:Interaction.Behaviors>
            </Label>
    

    However, the TestBehavior with the OneWayToSource binding within the DataTemplate did not work. The XAML for this (from the original question) is shown below:

                    <DataTemplate>
                        <StackPanel>
                            <Label Content="{Binding TestPropertyValue, ElementName=InnerTest}" Background="Cyan">
                                <b:Interaction.Behaviors>
                                    <tb:TestBehavior x:Name="InnerTest" TestPropertyValue="{Binding ItemTestViewModelValue, Mode=OneWayToSource}" />
                                </b:Interaction.Behaviors>
                            </Label>
                            <Label Content="{Binding ItemTestViewModelValue, Mode=OneWay}" Background="Lime" />
                        </StackPanel>
                    </DataTemplate>
    

    Adding UpdateSourceTrigger=PropertyChanged to the TestBehavior binding resulted in the view model property being updated correctly. The updated XAML is shown below:

                    <DataTemplate>
                        <StackPanel>
                            <Label Content="{Binding TestPropertyValue, ElementName=InnerTest}" Background="Cyan">
                                <b:Interaction.Behaviors>
                                    <tb:TestBehavior x:Name="InnerTest" TestPropertyValue="{Binding ItemTestViewModelValue, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}" />
                                </b:Interaction.Behaviors>
                            </Label>
                            <Label Content="{Binding ItemTestViewModelValue, Mode=OneWay}" Background="Lime" />
                        </StackPanel>
                    </DataTemplate>