【问题标题】:WPF binding OneWayToSource sets source property to "" when the DataContext is changed当 DataContext 更改时,WPF 绑定 OneWayToSource 将源属性设置为“”
【发布时间】:2014-08-15 09:33:43
【问题描述】:

我有一个 OneWayToSource 绑定,当我设置目标控件的 DataContext 时,它的行为不符合我的预期。源的属性被设置为默认值,而不是目标控件的属性值。

我在标准 WPF 窗口中创建了一个非常简单的程序来说明我的问题:

XAML

<StackPanel>
  <TextBox x:Name="tb"
    Text="{Binding Path=Text,Mode=OneWayToSource,UpdateSourceTrigger=PropertyChanged}"
    TextChanged="TextBox_TextChanged"/>

  <Button Content="Set DataContext" Click="Button1_Click"/>
</StackPanel>

MainWindow.cs

public partial class MainWindow : Window
{
   private ViewModel _vm = new ViewModel();

   private void Button1_Click(object sender, RoutedEventArgs e)
   {
      Debug.Print("'Set DataContext' button clicked");
      tb.DataContext = _vm;
   }

   private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
   {
      Debug.Print("TextBox changed to " + tb.Text);
   }
}

ViewModel.cs

public class ViewModel
{
   private string _Text;
   public string Text
   {
      get { return _Text; }
      set
      {
         Debug.Print(
            "ViewModel.Text (old value=" + (_Text ?? "<null>") + 
            ", new value=" + (value ?? "<null>") + ")");
         _Text = value;
      }
   }
}

TextBox tb 以 null DataContext 开始,因此绑定不会执行任何操作。因此,如果我在文本框中键入内容,例如“X”,ViewModel.Text 属性将保持为空。

如果我随后单击 Set DataContext 按钮,我会期望将 ViewModel.Text 属性设置为 TextBox.Text 属性的“X”。相反,它被设置为“”。当然绑定是有效的,因为如果我在文本框中输入“Y”,在“X”之后,它会将ViewModel.Text 属性设置为“XY”。

这是一个输出示例(最后两行由于计算顺序而违反直觉,但它们肯定都会在输入“Y”后立即出现):

文本框更改为 X
单击“设置 DataContext”按钮
ViewModel.Text(旧值=,新值=)
ViewModel.Text(旧值=,新值=XY)
文本框更改为 XY

为什么在设置 DataContext 时将ViewModel.Text 属性设置为“”而不是“X”?

我做错了什么?我错过了什么吗?我对绑定有什么误解吗?

编辑:我希望输出是:

文本框更改为 X
单击“设置 DataContext”按钮
ViewModel.Text(旧值=,新值=X
ViewModel.Text(旧值=X,新值=XY)
文本框更改为 XY

【问题讨论】:

    标签: c# wpf xaml binding


    【解决方案1】:

    这是一个错误,或者可能不是。微软声称它是设计使然。您首先键入 x,然后通过单击 Button 来终止 DataContext,因此为什么 TextBox 包含 x 并且您的 viewModel.Text 属性被新初始化(其为空)。当数据上下文发生变化时,仍然会调用 getter。最后你没有机会解决这个问题。

    但是,您可以使用两种方式并顺其自然。

    【讨论】:

    • 我可能不得不使用 TwoWay 并按照您的说法“顺其自然”。你有指向微软“这是设计使然”论点的链接吗?
    • 这里的重点是您对源绑定的一种方式有错误的印象。它没有按照您的期望进行,或者说您缺乏约束力知识。如果您查看有关绑定的文档,您会注意到绑定在目标和源之间建立了一座桥梁,但也会立即为目标设置一个值,因为目标首先初始化绑定。因此,无论是两种方式还是一种方式,绑定都将始终要求 getter。看看here
    【解决方案2】:

    在这里你必须UpdateSource,如下所示:

     private void Button1_Click(object sender, RoutedEventArgs e)
       {
    
          Debug.Print("'Set DataContext' button clicked");
          tb.DataContext = _vm;
          var bindingExp = tb.GetBindingExpression(TextBox.TextProperty);
          bingExp.UpdateSource();
       }
    

    【讨论】:

    • 这看起来很有希望,但它不起作用。它只是第二次将 ViewModel.Text 设置为 ""。
    • @stritch000 我可以知道为什么你有这个特殊的要求在按钮点击时设置文本框的 DataContext 吗?必须有优雅的方式来实现您要实现的目标
    【解决方案3】:

    TextBox 的 TextProperty 中有一个 Binding,当你设置 TextBox 的 DataContext 时,TextBox 会更新它的 source (viewmodel.Text) ,无论是哪种类型的 UpdateSourceTrigger。

    据说viewmodel中的第一个输出

    ViewModel.Text (old value=&lt;null&gt;, new value=)

    不是由UpdateSourceTrigger=PropertyChanged触发的。

    只是一个init的过程:

    private string _Text;
    public string Text
    {
        get { return _Text; }
        set
        {
            Debug.Print(
               "ViewModel.Text (old value=" + (_Text ?? "<null>") +
               ", new value=" + (value ?? "<null>") + ")");
            _Text = value;
        }
    }
    

    因为不是UpdateSourceTrigger=PropertyChanged触发的,所以viewmodel不会知道TextBox.Text的值。

    当你输入“Y”时,PropertyChanged的触发器会起作用,所以viewmodel会读取TextBox的文本。

    【讨论】:

    • 这肯定会达到最终结果,但它似乎有点麻烦,并且必须为每个这样的绑定完成。此外,它会导致 ViewModel 属性被设置两次(尽管我有一种直觉认为 是无法避免的)。
    • @stritch000 remove tb.Text = ""; 它不会导致 ViewModel 属性被设置两次
    • 这没什么区别。它仍然设置了两次。
    • @stritch000 那么,您的预期输出是什么?
    【解决方案4】:

    .NET 4 中存在一个错误,其中一种方法是源绑定,它为 OneWayToSource 绑定调用 getter,这就是您遇到此问题的原因。您可以通过在 tb.DataContext = _vm; 上放置断点来验证它。您会发现 setter 被调用,并且在该 getter 被调用 Text 属性之后。您可以通过在分配 datacontext 之前手动从视图中提供 viewmodel 值来解决您的问题。NET 4.5 解决了这个问题。 see herehere too

    private void Button1_Click(object sender, RoutedEventArgs e)
    {
       Debug.Print("'Set DataContext' button clicked");       
        _vm.Text=tb.Text;
        tb.DataContext = _vm;
    }
    

    【讨论】:

    • 这些链接都不是同一个问题(在 4.5 中未修复)。我的问题是 ViewModel.Text 属性被设置为空当数据上下文被更改时。否则绑定工作正常。
    • 因为一旦您应用数据上下文,就会调用 getter,并且视图的文本值会更新为 vm 的 _Text,即为空。您仍然会在 UI 中看到旧值,因为没有要刷新的事件它。您可以通过将绑定更改为 Two way 来验证这一点,一旦您按下按钮,它将变为 null。因为不应调用它的 One way binding getter(这是错误)。您可以使用我建议的代码这个案例。
    【解决方案5】:

    你需要Attached property:

    public static readonly DependencyProperty OneWaySourceRaiseProperty = DependencyProperty.RegisterAttached("OneWaySourceRaise", typeof(object), typeof(FrameworkElementExtended), new FrameworkPropertyMetadata(OneWaySourceRaiseChanged));
    
            public static object GetOneWaySourceRaise(DependencyObject o)
            {
                return o.GetValue(OneWaySourceRaiseProperty);
            }
    
            public static void SetOneWaySourceRaise(DependencyObject o, object value)
            {
                o.SetValue(OneWaySourceRaiseProperty, value);
            }
    
            private static void OneWaySourceRaiseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                if (e.NewValue == null)
                    return;
    
                var target = (FrameworkElement)d;
                target.Dispatcher.InvokeAsync(() =>
            {
                var bindings = target.GetBindings().Where(i => i.ParentBinding?.Mode == BindingMode.OneWayToSource).ToArray();
                foreach (var i in bindings)
                {
                    i.DataItem.SetProperty(i.ParentBinding.Path.Path, d.GetValue(i.TargetProperty));
                }
            });
    

    并在 XAML 中设置绑定:

    extendends:FrameworkElementExtended.OneWaySourceRaise="{Binding}"
    

    其中{Binding} - 绑定到DataContext。 你需要:

        public static IEnumerable<BindingExpression> GetBindings<T>(this T element, Func<DependencyProperty, bool> func = null) where T : DependencyObject
                {
                    var properties = element.GetType().GetDependencyProperties();
                    foreach (var i in properties)
                    {
                        var binding = BindingOperations.GetBindingExpression(element, i);
                        if (binding == null)
                            continue;
                        yield return binding;
                    }
                }
    
    
    private static readonly ConcurrentDictionary<Type, DependencyProperty[]> DependencyProperties = new ConcurrentDictionary<Type, DependencyProperty[]>();
        public static DependencyProperty[] GetDependencyProperties(this Type type)
                {
                    return DependencyProperties.GetOrAdd(type, t =>
                    {
                        var properties = GetDependencyProperties(TypeDescriptor.GetProperties(type, new Attribute[] { new PropertyFilterAttribute(PropertyFilterOptions.All) }));
                        return properties.ToArray();
                    });
                }
    
                private static IEnumerable<DependencyProperty> GetDependencyProperties(PropertyDescriptorCollection collection)
                {
                    if (collection == null)
                        yield break;
                    foreach (PropertyDescriptor i in collection)
                    {
                        var dpd = DependencyPropertyDescriptor.FromProperty(i);
                        if (dpd == null)
                            continue;
                        yield return dpd.DependencyProperty;
                    }
                }
    

    【讨论】: