在 DataTemplate 中使用时,行为 DependencyProperty 不更新 ViewModel

Behavior DependencyProperty not updating ViewModel when used within DataTemplate

我在 Behavior 中有一个 DependencyProperty,我正在为 OnAttached().

设置值

然后我将视图模型属性绑定到此 DependencyPropertyModeOneWayToSource

出于某种原因,绑定视图模型 属性 在 DataTemplate 中完成时不会被 OneWayToSource 绑定更新(永远不会调用视图模型的 setter ).在其他情况下它似乎工作正常。

我没有收到任何绑定错误,也没有看到任何异常等迹象,我不知道我做错了什么。

WPF 设计器 确实 显示了一些错误,声称 The member "TestPropertyValue" is not recognized or is not accessibleThe property "TestPropertyValue was not found in type 'TestBehavior',具体取决于您查看的位置。我不确定这些是否是 'real' 错误(据我观察,WPF Designer 在始终显示真正问题方面似乎并不完全可靠),如果是,它们是否与此问题或其他问题有关完全是问题。

如果这些 Designer 错误确实与此问题相关,我只能假设我一定是错误地声明了 DependencyProperty。如果是这样的话,我就看不出错误在哪里了。

我制作了一个复制该问题的示例项目。以下代码应该足够了,可以添加到名称为 WpfBehaviorDependencyPropertyIssue001.

的任何新 WPF 项目中
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;
    }
}

预期 调试输出消息(不包括标准 WPF 消息):

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

实际 调试输出消息(不包括标准 WPF 消息):

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

我完全测试了你的代码,它工作正常。

您的调试运行良好,因为在创建 MainViewModel 的实例时会立即调用所有成员。

MainTestValueA 被调用时值为 null,然后 OnPropertyChanged 被调用, bind 到标签控件被 TestPropertyValue [=35= 调用] 并使用初始化 example 并将其打印在输出上的 OnAttached 方法。

MainTestValueB 相同的步骤 对 ItemTestViewModelValue 重复相同的步骤,但因为它在 DataGridViewclr 不允许从视图访问。

当然这是我的结论

我已经设法解决了这个问题。

出于某种原因,在具有 OneWayToSourceModeDataTemplate 中进行绑定似乎需要 UpdateSourceTriggerPropertyChanged . 这样做会导致视图模型 属性 正确更新。

我通过实验发现了这一点,我不确定为什么这种行为与 DataTemplate 之外的绑定不同,尽管这种行为可能在某处有记录。

如果我能找到此行为的原因(记录或未记录),我将使用该信息更新此答案。

附加信息

为了让未来的读者清楚,带有 OneWayToSource 绑定 outside DataTemplate 的标签按预期工作。这个(来自原始问题)的 XAML 如下所示:

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

但是,TestBehaviorOneWayToSource 绑定 DataTemplate 无效。这个(来自原始问题)的 XAML 如下所示:

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

UpdateSourceTrigger=PropertyChanged 添加到 TestBehavior 绑定导致视图模型 属性 被正确更新。更新后的 XAML 如下所示:

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