首页 > 解决方案 > 刷为 DependencyProperty = 自动冻结?

问题描述

我在 Xaml 中定义了一个画笔:

<RadialGradientBrush x:Key="MyCoolBrush" MappingMode="Absolute" RadiusX="70" RadiusY="70">
    <RadialGradientBrush.GradientStops>
        <GradientStop Color="#FF000000" Offset="0" />
        <GradientStop Color="#00000000" Offset="0.6" />
    </RadialGradientBrush.GradientStops>
</RadialGradientBrush>

然后我有一个 DependencyProperty:

    public static readonly DependencyProperty MyCoolBrushProperty = DependencyProperty.Register(nameof(MyCoolBrush), typeof(Brush),
        typeof(MyCoolClass), new FrameworkPropertyMetadata(GetDefaultCoolBrush()));

GetDefaultCoolBrush 看起来像:

    private static Brush GetDefaultCoolBrush()
    {
        Brush brush = Application.Current.TryFindResource("MyCoolBrush") as Brush;

        if (brush == null)
            return null;

        return brush.Clone();
    }

我可以理解 TryFindResource 返回一个冻结的画笔,因为它在 Xaml 中定义,因此,我返回它的 Clone()。

问题是,当我尝试对 MyCoolBrush 做一些事情(通过 DP)时,我得到一个异常,说它是只读的。如果我尝试直接修改 GetDefaultCoolBrush() 的返回值,它工作正常。

为什么将画笔设置为 DP 会冻结它?这是预期的吗?我想在某种程度上,如果有人将 DP 设置为黑色,例如不能将其更改为绿色是有道理的,为什么不直接传递一个新的画笔呢?但是 GradialRadientBrushes() 设置起来有点昂贵,不是吗?真的,我想做的是移动画笔,所以我不想继续重新创建它,我只想更新中心点。

标签: c#wpf

解决方案


据我了解,这是因为可冻结设计与 DependencyObject 基础架构紧密耦合,出于同样的原因,该基础架构的行为类似于资源系统。

当在 XAML(例如 App.xaml)中定义像画笔、模板或样式这样的 FrameworkElements(或 DependencyObjects)时,它们对于应用程序是静态的,但在实例化之前不是任何可视化树的一部分。为了能够传递它们,它们将被密封(从 Dispatcher 系统中解开它们),这会导致 Freezable 类型冻结。设置默认值(通过 PropertyMetadata)时,这同样适用于 DependencyProperty。此默认值对于应用程序是静态的。因此,底层依赖系统必须密封这些静态值,以便能够在各个实例之间传递它们,以用作默认值。当您在类初始化后设置 DependecyProperty 时(例如,在Loaded已引发)实际实例值不再被冻结,因为它们与此特定实例耦合。

这是 Freezable.cs 的一个片段。当 DependencyProperty 在可冻结对象上调用 DependencyObject.Seal() 时,将调用 ISealable.Seal() 的覆盖,导致实例冻结:

/// <summary>
/// Seal this freezable
/// </summary>
void ISealable.Seal()
{
    Freeze();
 }

DependencyProperty.Register() 将调用其内部方法 RegisterCommon(),该方法通过调用调用 ValidateDefaultValueCommon() 的 ValidateMetadataDefaultValue() 来验证默认值。这个方法,在 DependencyProperty.cs 中定义,最终密封默认值:

 private static void ValidateDefaultValueCommon(
        object defaultValue,
        Type propertyType,
        string propertyName,
        ValidateValueCallback validateValueCallback,
        bool checkThreadAffinity)
    {
        // Ensure default value is the correct type
        if (!IsValidType(defaultValue, propertyType))
        {
            throw new ArgumentException(SR.Get(SRID.DefaultValuePropertyTypeMismatch, propertyName));
        }

        // An Expression used as default value won't behave as expected since
        //  it doesn't get evaluated.  We explicitly fail it here.
        if (defaultValue is Expression )
        {
            throw new ArgumentException(SR.Get(SRID.DefaultValueMayNotBeExpression));
        }

        if (checkThreadAffinity)
        {
            // If the default value is a DispatcherObject with thread affinity
            // we cannot accept it as a default value. If it implements ISealable
            // we attempt to seal it; if not we throw  an exception. Types not
            // deriving from DispatcherObject are allowed - it is up to the user to
            // make any custom types free-threaded.

            DispatcherObject dispatcherObject = defaultValue as DispatcherObject;

            if (dispatcherObject != null && dispatcherObject.Dispatcher != null)
            {
                // Try to make the DispatcherObject free-threaded if it's an
                // ISealable.

                ISealable valueAsISealable = dispatcherObject as ISealable;

                if (valueAsISealable != null && valueAsISealable.CanSeal)
                {
                    Invariant.Assert (!valueAsISealable.IsSealed,
                           "A Sealed ISealable must not have dispatcher affinity");

                    valueAsISealable.Seal();

                    Invariant.Assert(dispatcherObject.Dispatcher == null,
                        "ISealable.Seal() failed after ISealable.CanSeal returned true");
                }
                else
                {
                    throw new ArgumentException(SR.Get(SRID.DefaultValueMustBeFreeThreaded, propertyName));
                }
            }
        }

在上面的代码中,您可以找到以下注释:

如果默认值是具有线程关联性的 DispatcherObject,我们不能接受它作为默认值。如果它实现 ISealable,我们会尝试对其进行密封;如果不是,我们抛出一个异常。允许不从 DispatcherObject 派生的类型 - 用户可以自由处理任何自定义类型。

总结:Style、FrameworkTemplate、Brushes 或 Freezable(例如 Brush)等类型都实现了 ISealable,而 Freezable 提供的实现调用了 Freeze()。设置 DependencyProperty 的默认值会导致 ISealable.Seal() 被 DependencyProperty 调用。因此,您的克隆(将 IsFrozen 设置为 false)在分配给 PropertyMetadata 作为默认值时将再次冻结。由于您在此默认值上进行操作,因此在修改它时会出现异常。


推荐阅读