引言
最近因为一个触发器设置的结果总是不起效果的原因,进一步去了解[依赖属性的优先级](Dependency property value precedence - WPF .NET | Microsoft Learn)。在学习这个的过程中发现对SetCurrentValue一直以来的谬误。
在WPF中依赖属性Dependency property的三个方法SetValue 、SetCurrentValue、ClearValue。
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的现象如下:
ClearValue
执行后,绑定表达式为Null,也就是说ClearValue
之后,绑定的数据表达式会被清空SetValue
执行后,按钮颜色正常改变,绑定表达式不为Null。再次执行按钮4 修改依赖属性,按钮背景颜色也可以正常变化,也就是说SetValue
之后数据表达式不会被清空,仍然有效。SetCurrentValue
与第2点现象完全一致。
源码
通过查看源码,发现SetCurrentValue
和SetValue
都执行方法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来看,
- ClearValue会使得数据绑定失效。
- SetCurrentValue和官方文档所述一致,不会覆盖该属性的值来源,如果属性值是通过绑定、样式或触发器设置的,使用
SetCurrentValue
后,这些设置仍然有效。 - SetValue 设置的依赖属性为
DependencyProperty.UnsetValue,
会和ClearValue的表现一致。 - SetValue在Mode=TwoWay时,和SetCurrentValue表现一致,数据绑定不会失效。
- SetValue 在Mode=OneWay时。数据绑定也会失效。
参考
- What’s the difference between Dependency Property SetValue() & SetCurrentValue()
- Dependency property value precedence - WPF .NET | Microsoft Learn
- 源码