4

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

例如,我dependency property在类构造函数上设置了一个值。现在,当 Binding 初始化时,该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>

这是 MyCustomControl:

    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); }
        }
     }

这是视图模型:

    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;
     }
4

2 回答 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>
于 2013-09-13T08:29:02.817 回答
1

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

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

由于ctorInitializeComponent()中缺少,并且您可以在它之前或之后设置,让我们考虑这两种情况,假设在内部初始化。这里的初始化意味着它被分配了在 XAML 中声明的值。如果事先在本地设置,来自 XAML 的绑定将替换此值。如果它是在之后设置的,那么只要有机会进行评估,绑定就会考虑这个值,这对于TwoWayOneWayToSource绑定都是如此。(绑定评估的触发将在后面解释。)UserControlTxtTxtInitializeComponent()TxtTxt

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

<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. 初始化
    • (开始)InitializeComponent()进入Windowctor
    • Text初始化和双向绑定尝试附加绑定源
    • TextBox初始化
    • UserControl初始化
    • Txt初始化和OneWayToSource绑定尝试附加绑定源
    • Content初始化和OneWay绑定尝试附加绑定源
    • Button初始化
    • Window初始化
    • (完)InitializeComponent()Windowctor中退出
  2. 加载/渲染
    • (开始)退出Windowctor
    • 如果未附加,则双向绑定尝试附加绑定源
    • 如果未附加, OneWayToSource绑定尝试附加绑定源
    • 如果未附加,单向绑定尝试附加绑定源
    • Window已加载
    • TextBox已加载
    • UserControl已加载
    • Button已加载
    • (完)所有元素已加载
  3. 加载后
    • (开始)加载所有元素
    • 如果未附加,则双向绑定尝试附加绑定源
    • 如果未附加, OneWayToSource绑定尝试附加绑定源
    • 单向绑定初始尝试附加绑定源(如果未附加)
    • (完)Window显示

该属性的这种特殊行为UserControl随后被初始化,在这个问题中进行了讨论。如果您使用那里提供的方法,则调用OnInitialized覆盖以及触发Initialized事件将延迟到所有属性初始化之后。如果你BindingOperations.GetBindingExpression(this, MyCustomControl.TxtProperty)OnInitializedoverride 或 的处理程序中调用Initialized,返回值将不再为 null。

此时,分配本地值将是安全的。但是绑定评估不会立即触发传递值,因为绑定源(DataContext)仍然不可用,请注意 DataContext 直到Window初始化之后才设置。实际上,如果您检查Status返回的绑定表达式的属性,则值为Unattached.

进入 Loading 阶段后,第二次尝试附加绑定源将占用 DataContext,然后将通过绑定源的第一次附加触发评估,其中值Txt(在本例中为“123”)将Str通过 setter 传输到源属性。并且此绑定表达式的状态现在更改为Active代表绑定源的已解析状态。

如果您不使用该问题中提到的方法,您可以将InitializeComponent()Window 之后的本地值赋值移动到/的Intialized处理程序WindowLoaded处理程序中,结果将是相同的。除非在 中设置,否则本地分配将触发立即评估,因为绑定源已附加。而由第一个附件触发的将改为传输.WindowUserControlLoadedTxt

由绑定源更改触发的 OneWayToSource 绑定评估会将默认值传递给源属性。

如果我在运行时更改了 DataContext 怎么办?它会再次破坏受控制的依赖属性的值。

在上一节中,我们已经看到了两种类型的绑定评估触发器OneWayToSource,一种是目标属性更改(如果UpdateSourceTrigger绑定是PropertyChanged,这通常是默认值),另一种是绑定源的第一次附加。

从已接受答案中的讨论看来,您还有第二个问题,即为什么Txt在绑定源更改触发的评估中使用默认值而不是“当前”值。事实证明,这是第三种评估触发器的设计行为,这也得到了这个问题的第二和第三个答案的证实。顺便说一句,我在 .Net 4.5 中对此进行了测试,OneWayToSource通过在 setter 之后删除 getter 调用,从 4.0 开始的评估过程发生了变化,但这不会改变“默认值”行为。

附带说明一下,对于TwoWayOneWay绑定,由第一个附件触发的评估和绑定源的更改通过调用 getter 的行为完全相同。

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

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

PS:我知道这是一个旧帖子。但是由于这些行为仍然没有得到很好的记录,我认为这可能对任何试图了解发生了什么的人有所帮助。

于 2019-11-12T15:16:34.117 回答