WPF绑定OneWayToSource将源属性设置为";当DataContext被更改时

本文关键字:DataContext quot OneWayToSource 绑定 属性 设置 WPF | 更新日期: 2023-09-27 18:10:19

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

我在一个标准的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。文本属性保持为空。

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

下面是输出的一个示例(由于计算的顺序,最后两行是违反直觉的,但它们肯定都在输入"Y"之后立即出现):

文本框更改为X
点击"设置数据上下文"按钮
视图模型。Text (old value=, new value=)
视图模型。Text (old value=, new value=XY)
文本框更改为XY

为什么ViewModel.Text属性被设置为"而不是"X"当DataContext设置?

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

编辑:我希望输出是:

文本框更改为X
点击"设置数据上下文"按钮
视图模型。文本(旧值= & lt; null>新值= X )
视图模型。Text(旧值=X,新值=XY)
文本框更改为XY

WPF绑定OneWayToSource将源属性设置为";当DataContext被更改时

这是个bug,也许不是。微软声称这是设计出来的。您首先键入x,然后通过单击按钮杀死DataContext,因此为什么TextBox持有x和您的viewModel。文本属性刚初始化(为空)。当数据上下文发生变化时,getter仍然会被调用。最后,你没有机会解决这个问题。

这里你必须像下面这样设置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();
   }

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

据说viewmodel

中的第一个输出

" ViewModel.Text (old value=<null>, 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触发的,所以视图模型将不知道TextBox.Text的值。

当你输入"Y"时,PropertyChanged的触发器将工作,因此视图模型读取TextBox的文本

在。net 4中有一个错误,它为OneWayToSource绑定调用getter,这就是你遇到这个问题的原因。您可以通过在tb上添加断点来验证它。DataContext = _vm;你会发现setter被调用之后getter在Text属性上被调用。您可以通过在分配数据上下文之前手动从视图中输入视图模型值来解决这个问题。参见这里和这里

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

您需要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;
                }
            }