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绑定默认背景颜色的依赖属性
C# 复制代码
<Button
    x:Name="MyButton"
    Width="100"
    Height="50"
    Background="{Binding DefaultBackgroundColor,
                         Mode=TwoWay,
                         RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"
    Content="MyButton" />
C# 复制代码
 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的背景颜色属性,并判断绑定表达式是否为空
C# 复制代码
  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的背景颜色属性
C# 复制代码
 MyButton.SetCurrentValue(Button.BackgroundProperty, new SolidColorBrush(Colors.Orange));
IsBindingExpressionNull();
4. 按钮3 使用ClearValue 清楚背景颜色属性本地值
C# 复制代码
 // 清除本地值
 MyButton.ClearValue(Button.BackgroundProperty);
IsBindingExpressionNull();
5. 按钮4,修改依赖属性
C# 复制代码
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。

C# 复制代码
        private void SetValueChangeBackgroundUnsetValue_Click(object sender, RoutedEventArgs e)
        {
            MyButton.SetValue(Button.BackgroundProperty, DependencyProperty.UnsetValue);  //查看源码发现,UnsetValue时才会是清除本地值,并且 ClearValue
            IsBindingExpressionNull();
        }
SetValueCommon
C# 复制代码
        /// <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
C# 复制代码
/// <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,绑定被销毁。

C# 复制代码
<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. 源码
相关推荐
甜甜不吃芥末5 小时前
WPF依赖属性详解
wpf
Hat_man_8 小时前
WPF制作图片闪烁的自定义控件
wpf
晚安苏州2 天前
WPF Binding 绑定
wpf·wpf binding·wpf 绑定
wangnaisheng2 天前
【WPF】RenderTargetBitmap的使用
wpf
dotent·2 天前
WPF 完美解决改变指示灯的颜色
wpf
orangapple4 天前
WPF 用Vlc.DotNet.Wpf实现视频播放、停止、暂停功能
wpf·音视频
ysdysyn4 天前
wpf mvvm 数据绑定数据(按钮文字表头都可以),根据长度进行换行,并把换行的文字居中
c#·wpf·mvvm
orangapple4 天前
WPF 使用LibVLCSharp.WPF实现视频播放、停止、暂停功能
wpf
晚安苏州4 天前
WPF ControlTemplate 控件模板
wpf
晚安苏州4 天前
WPF 布局控件
wpf