将OneWayToSource绑定 - 奇怪的行为

4

最近我做了很多使用Binding Mode = OneWayToSource的测试,但仍然不知道为什么会发生某些事情。

例如,我在类构造函数中设置了一个dependency property的值。现在,当绑定初始化时,Target属性被设置为其默认值。这意味着dependency property被设置为null,我失去了在constructor中初始化的值。

为什么会发生这种情况?Binding Mode没有按照名称描述的方式工作。它应该只更新Source而不是Target

以下是代码:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MyViewModel();
    }

    private void OnClick(object sender, RoutedEventArgs e)
    {
        this.DataContext = new MyViewModel();
    }
}

这就是 XAML:

<StackPanel>
        <local:MyCustomControl Txt="{Binding Str, Mode=OneWayToSource}"/>
        <Button Click="OnClick"/>
</StackPanel>

这是我的自定义控件:
    public class MyCustomControl : Control
    {
        public static readonly DependencyProperty TxtProperty =
            DependencyProperty.Register("Txt", typeof(string), typeof(MyCustomControl), new UIPropertyMetadata(null));

        static MyCustomControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl)));
        }

        public MyCustomControl()
        {
           this.Txt = "123";
        }

        public string Txt
        {
           get { return (string)this.GetValue(TxtProperty); }

           set { this.SetValue(TxtProperty, value); }
        }
     }

这是ViewModel:
    public class MyViewModel : INotifyPropertyChanged
    {
        private string str;

        public string Str
        {
            get { return this.str; }
            set
            {
                if (this.str != value)
                {
                    this.str = value; this.OnPropertyChanged("Str");
                }
            }
         }

        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null && propertyName != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
     }

你能展示一些代码吗?因为我无法复制您所描述的内容。 - sa_ddam213
你不明白哪一部分? - ninja hedgehog
1
我理解了所有内容,只是无法复制。 - sa_ddam213
有代码。试一下。 - ninja hedgehog
Сйат░ЮУ»ЋтюеViewModelСИГУ«Йуй«private string str = "123"С║єтљЌ№╝Ъ - MoonKnight
绑定模式为OneWayToSource,那么在源中设置str = 123将如何初始化目标属性为123? - ninja hedgehog
2个回答

4
this.Txt = "123";

这是用本地值替换绑定的方法。请参见依赖属性值优先级。实际上,您是在调用DependencyObject.SetValue,而您真正想要的是DependencyProperty.SetCurrentValue。此外,您需要等到生命周期的后期才能执行此操作,否则WPF将两次更新Str:一次为“123”,然后再次为null
protected override void OnInitialized(EventArgs e)
{
    base.OnInitialized(e);
    this.SetCurrentValue(TxtProperty, "123");
}

如果您在用户控件的构造函数中执行此操作,则它将在WPF实例化时执行,但会在WPF加载、反序列化和应用您的BAML时立即被替换。
更新:抱歉,我误解了您的确切问题,但现在已经找到了一个可以复现的问题,如下所示。我错过了您随后更新DataContext的部分。我通过在数据上下文更改时设置当前值来解决了这个问题,但是在单独的消息中。否则,WPF会忽略将更改转发到新数据源的操作。
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

namespace SO18779291
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.setNewContext.Click += (s, e) => this.DataContext = new MyViewModel();
            this.DataContext = new MyViewModel();
        }
    }

    public class MyCustomControl : Control
    {
        public static readonly DependencyProperty TxtProperty =
            DependencyProperty.Register("Txt", typeof(string), typeof(MyCustomControl), new UIPropertyMetadata(OnTxtChanged));

        static MyCustomControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl)));
        }

        public MyCustomControl()
        {
            this.DataContextChanged += (s, e) =>
            {
                this.Dispatcher.BeginInvoke((Action)delegate
                {
                    this.SetCurrentValue(TxtProperty, "123");
                });
            };
        }

        public string Txt
        {
            get { return (string)this.GetValue(TxtProperty); }

            set { this.SetValue(TxtProperty, value); }
        }

        private static void OnTxtChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            Console.WriteLine("Changed: '{0}' -> '{1}'", e.OldValue, e.NewValue);
        }
    }

    public class MyViewModel : INotifyPropertyChanged
    {
        private string str;

        public string Str
        {
            get { return this.str; }
            set
            {
                if (this.str != value)
                {
                    this.str = value; this.OnPropertyChanged("Str");
                }
            }
        }

        protected void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null && propertyName != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

XAML:

<Window x:Class="SO18779291.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:SO18779291"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <local:MyCustomControl Txt="{Binding Str, Mode=OneWayToSource}"/>
        <Button x:Name="setNewContext">New Context</Button>
        <TextBlock Text="{Binding Str, Mode=OneWay}"/>
    </StackPanel>
</Window>

我也尝试过了,但它不起作用。你自己试试看吧。值保持为 null。让我们不要继续猜测和处理每种可能的情况。我想找到一个好的解决方案,或者至少知道为什么在使用 OneWayToSource 绑定时会发生这种情况。 - ninja hedgehog
@ninja。我确实尝试过,这不是猜测。它可以工作。你使用的WPF版本是什么? - Kent Boogaart
我正在使用4.0版本,在DataContextChanged事件中设置了Txt依赖属性的当前值。绑定确实将值从目标传输到源,但紧接着绑定又将空值从目标传输到源。所以两者都是null。对我来说它不起作用,我已经尝试过了。我现在就坐在那个WPF项目前。 - ninja hedgehog
@ninja:我更新了我的答案。你提到添加按钮的观点正是我所错过的。 - Kent Boogaart
顺便说一下,我认为这是WPF的一个限制。如果单向绑定到源的当前值没有明确设置,即使源本身发生更改,它也不会将其推送到源。编辑:是的,我认为这是一个错误。它会被修复吗?恐怕几乎没有机会。 - Kent Boogaart
显示剩余4条评论

2

只有在目标属性初始化后,绑定才能使用本地设置的值。

依赖属性被设置为null,我失去了在构造函数中初始化的值。为什么会发生这种情况?

由于您的UserControl构造函数中缺少InitializeComponent(),并且您可能在其之前或之后设置Txt,因此让我们考虑假设TxtInitializeComponent()中初始化的两种情况。在这里,Txt的初始化意味着它被分配了在XAML中声明的值。如果在此之前本地设置了Txt,则来自XAML的绑定将替换此值。如果是在之后设置,则绑定将在每次获得评估机会时考虑此值,这适用于TwoWayOneWayToSource绑定。(绑定评估的触发器将在稍后解释。)

为了证明我的理论,我进行了一个测试,其中包含三个元素,每个元素都有不同的绑定模式。

<TextBox Text="{Binding TwoWayStr,Mode=TwoWay}"></TextBox>
<local:UserControl1 Txt="{Binding OneWayToSourceStr, Mode=OneWay}" />
<Button Content="{Binding OneWayStr,Mode=OneWay}"></Button>   

然而,结果显示在两种情况下都忽略了本地值。因为与其他元素不同,当InitializeComponent()退出并且Initialized事件触发时,UserControl的属性还没有初始化,包括Txt
1. 初始化
- 进入Window构造函数中的InitializeComponent()(开始) - 初始化Text,并尝试使用TwoWay绑定来附加绑定源 - 初始化TextBox - 初始化UserControl - 初始化Txt,并尝试使用OneWayToSource绑定来附加绑定源 - 初始化Content,并尝试使用OneWay绑定来附加绑定源 - 初始化Button - 初始化Window - 退出Window构造函数中的InitializeComponent()(结束)
2. 加载/渲染
- 退出Window构造函数(开始) - 如果未连接,则尝试使用TwoWay绑定来附加绑定源 - 如果未连接,则尝试使用OneWayToSource绑定来附加绑定源 - 如果未连接,则尝试使用OneWay绑定来附加绑定源 - Window已加载 - TextBox已加载 - UserControl已加载 - Button已加载 - 所有元素已加载(结束)
3. 加载后
- 所有元素已加载(开始) - 如果未连接,则尝试使用TwoWay绑定来附加绑定源 - 如果未连接,则尝试使用OneWayToSource绑定来附加绑定源 - 如果未连接,则尝试使用OneWay初始绑定来附加绑定源 - Window显示 - 所有元素已加载(结束) UserControl的这种特殊行为,即在属性初始化之后进行初始化,是在此问题中讨论的。如果您使用该方法,将会延迟调用OnInitialized覆盖和触发Initialized事件,直到所有属性都被初始化。如果您在OnInitialized覆盖或Initialized处理程序中调用BindingOperations.GetBindingExpression(this, MyCustomControl.TxtProperty),则返回值将不再为空。
此时,可以安全地分配本地值。但是绑定评估不会立即触发以传输值,因为绑定源(DataContext)仍不可用,请注意DataContext在Window初始化之后才设置。实际上,如果检查返回的绑定表达式的Status属性,其值为Unattached
进入加载阶段后,第二次尝试连接绑定源将占用DataContext,然后将通过绑定源的第一个附加来触发评估,在此评估中,Txt的值(在本例中为"123")将通过setter传递到源属性Str。此绑定表达式的状态现在更改为Active,表示已解析绑定源的状态。
如果您不使用该问题中提到的方法,则可以将本地值分配移动到窗口的InitializeComponent()WindowInitialized处理程序或Window/UserControlLoaded处理程序之后,结果将相同。只有在Loaded中设置时,本地分配将立即触发评估,因为绑定源已经附加。而由第一个附加触发的评估将转而传输Txt的默认值。 OneWayToSource绑定的评估由绑定源更改触发,将默认值传递给源属性。
“如果我在运行时更改DataContext会怎么样?这将再次破坏控件中依赖属性的值。”
从前一段中,我们已经看到了OneWayToSource绑定的两种触发器类型,一种是目标属性更改(如果绑定的UpdateSourceTriggerPropertyChanged,通常为默认值),另一种是绑定源的第一个附加。
似乎从接受的答案的讨论中可以得出您有一个关于为什么在绑定源更改触发的评估中使用默认值而不是Txt的“当前”值的第二个问题。事实证明,这是这种第三种评估触发器的设计行为,这也得到了此问题的第二和第三个答案的确认。顺便说一下,在.Net 4.5中进行测试,OneWayToSource的评估过程从4.0中删除了setter后的getter调用,但这并不改变“默认值”的行为。
作为旁注,对于TwoWayOneWay绑定,由第一个附加和绑定源更改触发的评估完全相同,都会调用getter。

额外说明:OneWayToSource绑定将忽略所有级别的路径更改

另一个可能与主题有关的OneWayToSource绑定的奇怪行为是,尽管预期不会监听目标属性的更改,但如果绑定路径包含多个级别,这意味着目标属性是嵌套的,则从目标属性向上的所有级别的更改也将被忽略。例如,如果像这样声明绑定Text={Binding ChildViewModel.Str, Mode=OneWayToSource},则对ChildViewModel属性的更改不会触发绑定评估,实际上,如果您通过更改Text来触发评估,Str setter会在先前的ChildViewModel实例上调用。此行为使OneWayToSource与其他两种模式相比更加偏离。

附言:我知道这是一篇旧文章。但由于这些行为仍未得到很好的记录,我认为这可能有助于任何试图理解发生情况的人。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接