【问题标题】:Binding OneWayToSource - Strange Behavior绑定 OneWayToSource - 奇怪的行为
【发布时间】:2013-09-13 06:01:59
【问题描述】:

我最近对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); }
        }
     }

这是 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;
     }

【问题讨论】:

  • 你能显示一些代码吗,因为我无法复制你所描述的内容
  • 你没有得到哪一部分?
  • 我都得到了,只是无法复制。
  • 有代码。试试看。
  • 您是否尝试在 ViewModel 中设置 private string str = "123"

标签: c# wpf


【解决方案1】:
this.Txt = "123";

这是 用本地值替换您的绑定。见dependency property value precedence。当您真正想要DependencyProperty.SetCurrentValue 时,您实际上是在调用DependencyObject.SetValue。此外,您需要等到生命周期的后期才能执行此操作,否则 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>

【讨论】:

  • OnInitalized 在控件完全初始化时仅触发一次,但如果我在运行时更改 DataContext 会怎样?它会再次破坏受控制的依赖属性的值。就像我在我的问题中提到的那样,我做了一堆测试,并且我还尝试了使用 OnInitalized 事件的方法,但是一旦 DataContext 更改,Binding 就会将依赖属性的值设置回其默认值(即为空)。但这不应该发生,因为 Binding.Mode 是 OneWayToSource。
  • DataContextChanged += (s, e) =&gt; this.SetCurrentValue(TxtProperty, "123");
  • 我也试过了,但它不起作用。自己尝试一下。该值保持为空。让我们不要继续猜测和处理所有可能的情况。我想找到一个好的解决方案,或者至少是在使用 OneWayToSource Binding 时发生这种情况的原因
  • @ninja。我确实试过了——这不是猜测。有用。您使用的是哪个版本的 WPF?
  • 我使用的是 4.0,在 DataContextChanged 上我设置了 Txt 依赖属性的当前值。绑定确实将值从目标传输到源,但之后绑定再次将空值从目标传输到源。所以两者都是空的。对我来说它不起作用,我也试过了。我现在正坐在那个 wpf 项目前面。
【解决方案2】:

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

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

由于UserControl ctor 中缺少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. 初始化
    • (开始)在Windowctor 中输入InitializeComponent()
    • Text 已初始化和 TwoWay 绑定尝试附加绑定源
    • TextBox已初始化
    • UserControl已初始化
    • Txt 已初始化和 OneWayToSource 绑定尝试附加绑定源
    • Content 初始化和OneWay绑定尝试附加绑定源
    • Button已初始化
    • Window已初始化
    • (结束)在Window ctor 中退出InitializeComponent()
  2. 加载/渲染
    • (开始)退出Windowctor
    • TwoWay 绑定尝试在未附加的情况下附加绑定源
    • OneWayToSource 绑定尝试在未附加的情况下附加绑定源
    • OneWay 绑定尝试在未附加的情况下附加绑定源
    • Window已加载
    • TextBox已加载
    • UserControl已加载
    • Button已加载
    • (结束)已加载所有元素
  3. 加载后
    • (开始)已加载所有元素
    • TwoWay 绑定尝试在未附加的情况下附加绑定源
    • OneWayToSource 绑定尝试在未附加的情况下附加绑定源
    • OneWay 绑定初始尝试附加绑定源(如果未附加)
    • (完)Window已显示

UserControl 的这种特殊行为(属性在之后被初始化)在this question 中讨论。如果您使用那里提供的方法,OnInitialized 覆盖的调用以及Initialized 事件的触发将延迟到所有属性初始化之后。而如果你在OnInitialized override 或者Initialized 的handler 中调用BindingOperations.GetBindingExpression(this, MyCustomControl.TxtProperty),返回值将不再为null。

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

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

如果您不使用该问题中提到的方法,您可以将Window的InitializeComponent()之后的局部值赋值移动到WindowIntialized处理程序或Window/的Loaded处理程序中UserControl,结果是一样的。除非它在Loaded 中设置,否则本地分配将触发立即评估,因为绑定源已经附加。而第一个附件触发的将改为传输默认值Txt

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

如果我在运行时更改了 DataContext 会怎样?它会再次摧毁 控件中依赖属性的值。

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

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-05-17
    • 2020-04-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-30
    • 2013-07-01
    • 2021-05-03
    • 2016-08-03
    相关资源
    最近更新 更多