当前位置: 首页 > article >正文

WPF|依赖属性SetCurrentValue方法不会使绑定失效, SetValue方法会使绑定失效?是真的吗?

引言

最近因为一个触发器设置的结果总是不起效果的原因,进一步去了解[依赖属性的优先级](Dependency property value precedence - WPF .NET | Microsoft Learn)。在学习这个的过程中发现对SetCurrentValue一直以来的谬误。

在WPF中依赖属性Dependency property的三个方法SetValue 、SetCurrentValue、ClearValue。

  1. SetCurrentValue 方法用于设置依赖属性的当前值,但不会覆盖该属性的值来源。这意味着,如果属性值是通过绑定、样式或触发器设置的,使用 SetCurrentValue 后,这些设置仍然有效。

然而这很容易让人以为SetValue 方法会使得数据绑定失效。就像先执行了ClearValue 一样。网上我看到的很多文章也这么说,这让我困惑了很久,但我实际操作下来,并非如此,实际上并不是。

为了验证这三个方法,我以设置按钮背景颜色为例,写了一个Demo。

其主要作用如下:

1. myButton绑定默认背景颜色的依赖属性
<Button
    x:Name="MyButton"
    Width="100"
    Height="50"
    Background="{Binding DefaultBackgroundColor,
                         Mode=TwoWay,
                         RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"
    Content="MyButton" />
 public static readonly DependencyProperty DefaultBackgroundColorProperty =
     DependencyProperty.Register(
         "DefaultBackgroundColor",
         typeof(Brush),
         typeof(MainWindow),
         new PropertyMetadata(Brushes.Pink,OnDefaultBackgroundColorChanged)
     );

 private static void OnDefaultBackgroundColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
 {
     MessageBox.Show("DefaultBackgroundColor changed");
 }
 public static readonly DependencyProperty DefaultForegroundColorProperty =
     DependencyProperty.Register(
         "DefaultForegroundColor",
         typeof(Brush),
         typeof(MainWindow),
         new PropertyMetadata(Brushes.Gray,OnDefaultForegroundChanged)
     );
     
 
2. 按钮1使用SetValue设置myButton的背景颜色属性,并判断绑定表达式是否为空
  private void SetValueChangeBackground_Click(object sender, RoutedEventArgs e)
  {
      MyButton.SetValue(Button.BackgroundProperty, new SolidColorBrush(Colors.Green));
      IsBindingExpressionNull();
  }
  
  private void IsBindingExpressionNull()
{ 
    if (MyButton.GetBindingExpression(Button.BackgroundProperty) == null)
    {
        MessageBox.Show("BindingExpression is null");
    }
}
3. 按钮2使用SetCurrentValue设置myButton的背景颜色属性
 MyButton.SetCurrentValue(Button.BackgroundProperty, new SolidColorBrush(Colors.Orange));
IsBindingExpressionNull();
4. 按钮3 使用ClearValue 清楚背景颜色属性本地值
 // 清除本地值
 MyButton.ClearValue(Button.BackgroundProperty);
IsBindingExpressionNull();
5. 按钮4,修改依赖属性
DefaultBackgroundColor = new SolidColorBrush(Colors.LightGreen);
我使用.NET 8 做的Demo的现象如下:
  1. ClearValue执行后,绑定表达式为Null,也就是说ClearValue之后,绑定的数据表达式会被清空
  2. SetValue执行后,按钮颜色正常改变,绑定表达式不为Null。再次执行按钮4 修改依赖属性,按钮背景颜色也可以正常变化,也就是说SetValue之后数据表达式不会被清空,仍然有效。
  3. SetCurrentValue与第2点现象完全一致。

源码

通过查看源码,发现SetCurrentValueSetValue都执行方法SetValueCommon,只是入参coerceWithCurrentValue不同,// SetValue时为false 和SetCurrentValue时为true。

并且推测当SetValue的value入参等于DependencyProperty.UnsetValue时,应当会和ClearValueCommon执行相同的方法。

以下是测试代码,实际测试结果也是如此,此时绑定表达式为Null。


        private void SetValueChangeBackgroundUnsetValue_Click(object sender, RoutedEventArgs e)
        {
            MyButton.SetValue(Button.BackgroundProperty, DependencyProperty.UnsetValue);  //查看源码发现,UnsetValue时才会是清除本地值,并且 ClearValue
            IsBindingExpressionNull();
        }
SetValueCommon
        /// <summary>
        ///     The common code shared by all variants of SetValue
        /// </summary>
        // Takes metadata from caller because most of them have already retrieved it
        //  for their own purposes, avoiding the duplicate GetMetadata call.
        private void SetValueCommon(
            DependencyProperty  dp,
            object              value,
            PropertyMetadata    metadata,
            bool                coerceWithDeferredReference,
            bool                coerceWithCurrentValue, // SetValue时为false 和SetCurrentValue时为true
            OperationType       operationType,
            bool                isInternal)
        {
            if (IsSealed)
            {
                throw new InvalidOperationException(SR.Get(SRID.SetOnReadOnlyObjectNotAllowed, this));
            }
 
            Expression newExpr = null;
            DependencySource[] newSources = null;
 
            EntryIndex entryIndex = LookupEntry(dp.GlobalIndex);
 
            // Treat Unset as a Clear
            if( value == DependencyProperty.UnsetValue )
            {
                Debug.Assert(!coerceWithCurrentValue, "Don't call SetCurrentValue with UnsetValue");
                // Parameters should have already been validated, so we call
                //  into the private method to avoid validating again.
                ClearValueCommon(entryIndex, dp, metadata);
                return;
            }
 
            // Validate the "value" against the DP.
            bool isDeferredReference = false;
            bool newValueHasExpressionMarker = (value == ExpressionInAlternativeStore);
 
            // First try to validate the value; only after this validation fails should we
            // do the more expensive checks (type checks) for the less common scenarios
            if (!newValueHasExpressionMarker)
            {
                bool isValidValue = isInternal ? dp.IsValidValueInternal(value) : dp.IsValidValue(value);
 
                // for properties of type "object", we have to always check for expression & deferredreference
                if (!isValidValue || dp.IsObjectType)
                {
                    // 2nd most common is expression
                    newExpr = value as Expression;
                    if (newExpr != null)
                    {
                        // For Expressions, perform additional validation
                        // Make sure Expression is "attachable"
                        if (!newExpr.Attachable)
                        {
                            throw new ArgumentException(SR.Get(SRID.SharingNonSharableExpression));
                        }
 
                        // Check dispatchers of all Sources
                        // CALLBACK
                        newSources = newExpr.GetSources();
                        ValidateSources(this, newSources, newExpr);
                    }
                    else
                    {
                        // and least common is DeferredReference
                        isDeferredReference = (value is DeferredReference);
                        if (!isDeferredReference)
                        {
                            if (!isValidValue)
                            {
                                // it's not a valid value & it's not an expression, so throw
                                throw new ArgumentException(SR.Get(SRID.InvalidPropertyValue, value, dp.Name));
                            }
                        }
                    }
                }
            }
 
            // Get old value
            EffectiveValueEntry oldEntry;
            if (operationType == OperationType.ChangeMutableDefaultValue)
            {
                oldEntry = new EffectiveValueEntry(dp, BaseValueSourceInternal.Default);
                oldEntry.Value = value;
            }
            else
            {
                oldEntry = GetValueEntry(entryIndex, dp, metadata, RequestFlags.RawEntry);
            }
 
            // if there's an expression in some other store, fetch it now
            Expression currentExpr =
                    (oldEntry.HasExpressionMarker)  ? _getExpressionCore(this, dp, metadata)
                  : (oldEntry.IsExpression)         ? (oldEntry.LocalValue as Expression)
                  :                                   null;
 
            // Allow expression to store value if new value is
            // not an Expression, if applicable
 
            bool handled = false;
            if ((currentExpr != null) && (newExpr == null))
            {
                // Resolve deferred references because we haven't modified
                // the expression code to work with DeferredReference yet.
                if (isDeferredReference)
                {
                    value = ((DeferredReference) value).GetValue(BaseValueSourceInternal.Local);
                }
 
                // CALLBACK
                handled = currentExpr.SetValue(this, dp, value);
                entryIndex = CheckEntryIndex(entryIndex, dp.GlobalIndex);
            }
 
            // Create the new effective value entry
            EffectiveValueEntry newEntry;
             if (handled)                                                                                                                                                                                        )
            {
                // If expression handled set, then done
                if (entryIndex.Found)
                {
                    newEntry = _effectiveValues[entryIndex.Index];
                }
                else
                {
                    // the expression.SetValue resulted in this value being removed from the table;
                    // use the default value.
                    newEntry = EffectiveValueEntry.CreateDefaultValueEntry(dp, metadata.GetDefaultValue(this, dp));
                }
 
                coerceWithCurrentValue = false; // expression already handled the control-value
            }
            else
            {
                 // allow a control-value to coerce an expression value, when the
                // expression didn't handle the value
                if (coerceWithCurrentValue && currentExpr != null)
                {
                    currentExpr = null;
                }
 
                newEntry = new EffectiveValueEntry(dp, BaseValueSourceInternal.Local);
 
                // detach the old expression, if applicable
                if (currentExpr != null)
                {
                    // CALLBACK
                    DependencySource[] currentSources = currentExpr.GetSources();
 
                    UpdateSourceDependentLists(this, dp, currentSources, currentExpr, false);  // Remove
 
                    // CALLBACK
                    currentExpr.OnDetach(this, dp);
                    currentExpr.MarkDetached();
                    entryIndex = CheckEntryIndex(entryIndex, dp.GlobalIndex);
                }
 
                // attach the new expression, if applicable
                if (newExpr == null)
                {
                    // simple local value set
                    newEntry.HasExpressionMarker = newValueHasExpressionMarker;
                    newEntry.Value = value;
                }
                else
                {
                    Debug.Assert(!coerceWithCurrentValue, "Expression values not supported in SetCurrentValue");
 
                    // First put the expression in the effectivevalueentry table for this object;
                    // this allows the expression to update the value accordingly in OnAttach
                    SetEffectiveValue(entryIndex, dp, dp.GlobalIndex, metadata, newExpr, BaseValueSourceInternal.Local);
 
                    // Before the expression is attached it has default value
                    object defaultValue = metadata.GetDefaultValue(this, dp);
                    entryIndex = CheckEntryIndex(entryIndex, dp.GlobalIndex);
                    SetExpressionValue(entryIndex, defaultValue, newExpr);
                    UpdateSourceDependentLists(this, dp, newSources, newExpr, true);  // Add
 
                    newExpr.MarkAttached();
 
                    // CALLBACK
                    newExpr.OnAttach(this, dp);
 
                    // the attach may have added entries in the effective value table ...
                    // so, update the entryIndex accordingly.
                    entryIndex = CheckEntryIndex(entryIndex, dp.GlobalIndex);
 
                    newEntry = EvaluateExpression(
                            entryIndex,
                            dp,
                            newExpr,
                            metadata,
                            oldEntry,
                            _effectiveValues[entryIndex.Index]);
 
                    entryIndex = CheckEntryIndex(entryIndex, dp.GlobalIndex);
                }
            }
 
            UpdateEffectiveValue(
                entryIndex,
                dp,
                metadata,
                oldEntry,
                ref newEntry,
                coerceWithDeferredReference,
                coerceWithCurrentValue,
                operationType);
        }
ClearValueCommon
/// <summary>
        ///     The common code shared by all variants of ClearValue
        /// </summary>
        private void ClearValueCommon(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata)
        {
            if (IsSealed)
            {
                throw new InvalidOperationException(SR.Get(SRID.ClearOnReadOnlyObjectNotAllowed, this));
            }
 
            // Get old value
            EffectiveValueEntry oldEntry = GetValueEntry(
                                        entryIndex,
                                        dp,
                                        metadata,
                                        RequestFlags.RawEntry);
 
            // Get current local value
            // (No need to go through read local callback, just checking
            // for presence of Expression)
            object current = oldEntry.LocalValue;
 
            // Get current expression
            Expression currentExpr = (oldEntry.IsExpression) ? (current as Expression) : null;
 
            // Inform value expression of detachment, if applicable
            if (currentExpr != null)
            {
                // CALLBACK
                DependencySource[] currentSources = currentExpr.GetSources();
 
                UpdateSourceDependentLists(this, dp, currentSources, currentExpr, false);  // Remove
 
                // CALLBACK
                currentExpr.OnDetach(this, dp);
                currentExpr.MarkDetached();
                entryIndex = CheckEntryIndex(entryIndex, dp.GlobalIndex);
            }
 
            // valuesource == Local && value == UnsetValue indicates that we are clearing the local value
            EffectiveValueEntry newEntry = new EffectiveValueEntry(dp, BaseValueSourceInternal.Local);
 
            // Property is now invalid
            UpdateEffectiveValue(
                    entryIndex,
                    dp,
                    metadata,
                    oldEntry,
                    ref newEntry,
                    false /* coerceWithDeferredReference */,
                    false /* coerceWithCurrentValue */,
                    OperationType.Unknown);
        }
 

Mode对SetValue的影响

在wpf - 依赖属性SetValue()和SetCurrentValue()之间的区别是什么 - 堆栈溢出 — wpf - What’s the difference between Dependency Property SetValue() & SetCurrentValue() - Stack Overflow上看到和Mode还有关系,于是我又测试了一下:
在这里插入图片描述

将按钮的绑定方式改为OneWay,发现SetValue执行后,绑定为Null,绑定被销毁。

<Button
    x:Name="MyButton"
    Width="100"
    Height="50"
    Background="{Binding DefaultBackgroundColor,
                         Mode=OneWay,
                         RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"
    Content="MyButton" />

总结

就我的测试Demo来看,

  1. ClearValue会使得数据绑定失效。
  2. SetCurrentValue和官方文档所述一致,不会覆盖该属性的值来源,如果属性值是通过绑定、样式或触发器设置的,使用 SetCurrentValue 后,这些设置仍然有效。
  3. SetValue 设置的依赖属性为DependencyProperty.UnsetValue,会和ClearValue的表现一致。
  4. SetValue在Mode=TwoWay时,和SetCurrentValue表现一致,数据绑定不会失效。
  5. SetValue 在Mode=OneWay时。数据绑定也会失效。

参考

  1. What’s the difference between Dependency Property SetValue() & SetCurrentValue()
  2. Dependency property value precedence - WPF .NET | Microsoft Learn
  3. 源码

http://www.kler.cn/news/342177.html

相关文章:

  • 2024.10月7~10日 进一步完善《电信资费管理系统》
  • 自动驾驶系列—从IMU到惯性定位算法:自动驾驶精准定位的幕后科技
  • 制造业人工智能的场景应用落地现状、难点和建议
  • 力扣10.9
  • 【数据结构】6道经典链表面试题
  • Ubuntu 更换内核版本
  • 单目三d重建学习笔记2024
  • 从开发效率到查询性能:JPA 和 MyBatis 在企业系统中的完美结合
  • Git 工作区、暂存区和仓库
  • 跟《经济学人》学英文:2024年10月05日这期 Workouts for the face are a growing business
  • python画图|步进图基本教程
  • 【C语言系统编程】【第三部分:网络编程】3.3 实践与案例分析
  • 解读 AI 获客关键要素,开启营销新未来
  • 架构设计(14)分布式系统的CAP,BASE与ACID
  • JavaScript 网页设计案例详解
  • xtu oj 四位数
  • Mybatis-Plus分页和根据日期查询数据
  • HTML5+Css3(背景属性background)
  • 力扣1930. 长度为3的不同回文子序列
  • App推广新利器:Xinstall带你直达指定页面