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

Posted

技术标签:

【中文标题】在 DataTemplate 中使用时,行为 DependencyProperty 不更新 ViewModel【英文标题】:Behavior DependencyProperty not updating ViewModel when used within DataTemplate 【发布时间】:2021-09-20 03:43:23 【问题描述】:

我在Behavior 中有一个DependencyProperty,我正在为OnAttached() 设置值。

然后我将视图模型属性绑定到这个DependencyProperty,其中ModeOneWayToSource

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

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

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

如果这些设计器错误确实与此问题有关,我只能假设我必须错误地声明了 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

【问题讨论】:

我认为附加的道具应该使用“RegisterAttached”注册。同样在 ctor 中,您将值“Example”设置为 TestBehaviour 对象本身的实例,而在静态 Get/Set 方法中,您从传入的依赖对象获取/设置值,它们是控件/标签你的xml。所以这些值被映射到不同的目标...... @lidqy TestPropertyValue 不是附加属性。它是TestBehavior 的依赖属性。标签本身的绑定是为了证明问题。要关注的关键点是TestBehavior 元素本身的绑定。此外,此示例中未使用行为的AssociatedObject,这意味着依赖属性值与标签一起存储。 @lidqy 另外,这不是作为附加属性开始的原因是因为这最终将用于我 am 使用的更大项目中AssociatedObjectBehavior。在重现我遇到的问题时,这些功能是多余的。最后,“Example”的值不是在构造函数中设置的,而是在行为的OnAttached() 方法中设置的。 【参考方案1】:

我完全测试了你的代码,它运行良好。

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

MainTestValueA 使用 null 值调用,然后调用 OnPropertyChanged 并使用 TestPropertyValue 属性调用标签控件的 bind,并使用初始化 exampleOnAttached 方法并将其打印在输出上。

MainTestValueB 的步骤相同 并且对ItemTestViewModelValue 重复相同的步骤,但因为它位于DataGridView 内部,clr 不允许从 View 访问。

当然,这是我的结论。

【讨论】:

1/2 您提供的更改并不能解决问题,而且我承认,在问题的上下文中没有多大意义(尽管我确实尝试过)。首先,您添加了一个本地 TestValueProperty 属性,但没有删除静态 GetTestPropertyValueSetTestPropertyValue 方法。这是在 XAML 中识别依赖属性的两种不同方法。两者兼有是没有意义的,但它们之间没有功能上的区别。 (续) 2/2 其次,在它自己的 PropertyChanged 回调中为属性调用 SetValue() 似乎很奇怪。你这背后的原因是什么?如果 WPF 没有将引发事件限制为 ,则当依赖项属性的有效值发生更改时,这将导致无限循环。您实际上是在说“如果依赖属性值已更改,请再次将其更改为我们刚刚更改为相同的值”。第三,我明确希望属性为OneWayToSourcenot TwoWay。我不希望视图模型更新依赖属性值,只读取它。 响应您的编辑,我知道我在问题中提供的代码与DataTemplate 中的Behavior 绑定不同,没有错误等,它只是无法设置视图模型属性的值。您可能会在回答的最后提到这一点,您说“但是因为它在 DataGridView clr 内部不允许从 View 访问。”,但是恐怕我不确定你是什么正想说。普通数据绑定通常在RowDetailsTemplateDataGrid 中工作正常。你能详细说明你的意思吗? 我跟踪了程序,当我到达 ItemTestViewModelValue 时,调试器返回到值为“Example”的项目,并且因为项目绑定到 DataGridView 我做了这个 clr 没有输入定义(公共字符串 itemTestViewModelValue )。 如果我理解你所说的正确,你已经发现了我的问题所问的问题。我很清楚它不会调用 ItemTestViewModelValue 设置器,我努力通过添加调试消息来使这一事实显而易见在 Visual Studio 中。我还在问题本身中包含了输出消息,以尝试使其尽可能清晰。我正在尝试找出为什么会发生这种行为以及是否可以解决此问题。【参考方案2】:

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

由于某种原因,对于具有 ModeOneWayToSourceDataTemplate 内的绑定,看起来需要 UpdateSourceTriggerPropertyChanged。 这样做会导致正确更新视图模型属性。

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

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

附加信息

为了让未来的读者更清楚,DataTemplateOneWayToSource 绑定outside 的标签按预期工作。这个(来自原始问题)的 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>

【讨论】:

以上是关于在 DataTemplate 中使用时,行为 DependencyProperty 不更新 ViewModel的主要内容,如果未能解决你的问题,请参考以下文章

使用 DataTemplate 时,ListView 仅显示列表中的最后一项

如何使用类似表格的 DataTemplate 在 UWP ListView 中动态缩放列宽

如何设置控件属性(在DataTemplate和UserControl中)的绑定以使用ItemSource的给定属性?

发生事件时在 DataTemplate 中运行 Storyboard

使用 Trigger 更改 DataTemplate 时随机选择的项目

WPF Template模版之DataTemplate与ControlTemplate的关系和应用