Silverlight/WPF中DependencyProperty使用陷阱一枚

  

今天有朋友写Silverlight代码遇到一个问题,让我一起看一下。这是他写的一个测试类:

class Foo : DependencyObject

{

    public List<int> Bars

    {

        get { return (List<int>)GetValue (BarsProperty); }

        set { SetValue (BarsProperty, value); }

    }



    public static readonly DependencyProperty BarsProperty =

        DependencyProperty.Register (

            "Bars",

            typeof (List<int>),

            typeof (Foo),

            new PropertyMetadata (new List<int> ()));

}

使用代码如下:

可见,当foo2刚刚创建时,Bars中已经有一个元素了。

SL/WPF达人们看到这里肯定已经笑了,我刚开始学习DependencyProperty时也曾在这个问题上卡住。

在Immediate Window中测试foo1与foo2的Bars属性:

果然,Bars其实是同一个List<int>对象。

朋友说,我上网查了,已经知道像List这些集合类属性不能用DependencyProperty Metadata的defaultValue来初始化,必须要在类的构造器中初始化(参考http://msdn.microsoft.com/zh-cn/library/cc903961(v=VS.95).aspx):

class Foo : DependencyObject

{

    public List<int> Bars

    {

        get { return (List<int>)GetValue (BarsProperty); }

        set { SetValue (BarsProperty, value); }

    }



    public static readonly DependencyProperty BarsProperty =

        DependencyProperty.Register (

            "Bars",

            typeof (List<int>),

            typeof (Foo),

            new PropertyMetadata (null));



    public Foo ()

    {

        Bars = new List<int> ();

    }

}

这样就能得到正常结果。但他不明白的是,为什么微软不为这种情况特别处理一下,而要留给开发人员这么一个陷阱?

说实话,我当时被问住了,因为我之前也只是机械地把结论当做一个开发的“注意事项”记了下来,至于原因,并没有深思。

---------------------

其实,从原理上思考一下,并不难得出结论。微软并不是不愿意处理,而是无法处理。

首先,这个问题并不是只有集合类型值才会遇到,所有的引用类型都可能遇到此问题,参考下面的示例:

class Foo : DependencyObject

{

    public List<int> Bars

    {

        get { return (List<int>)GetValue (BarsProperty); }

        set { SetValue (BarsProperty, value); }

    }



    public static readonly DependencyProperty BarsProperty =

        DependencyProperty.Register (

            "Bars",

            typeof (List<int>),

            typeof (Foo),

            new PropertyMetadata (new List<int> ()));



    public int Length

    {

        get { return (int)GetValue (LengthProperty); }

        set { SetValue (LengthProperty, value); }

    }



    public static readonly DependencyProperty LengthProperty =

        DependencyProperty.Register (

            "Length",

            typeof (int),

            typeof (Foo),

            new PropertyMetadata (5));



    public class Data

    {

        public int Value { get; set; }

    }



    public Data MyData

    {

        get { return (Data)GetValue (MyDataProperty); }

        set { SetValue (MyDataProperty, value); }

    }



    public static readonly DependencyProperty MyDataProperty =

        DependencyProperty.Register (

            "MyData",

            typeof (Data),

            typeof (Foo),

            new PropertyMetadata (new Data () { Value = 3 }));

}

该类声明了三个DP:List<int>类型的Bars,int类型的Length和自定义类Data类型的MyData,均用DP的defaultValue进行初始化。调试结果如下:

可见,在foo2刚刚构造完成时,Bars和MyData属性都已经和foo1一致了,使用object.ReferenceEquals比较后证明确实均为同一实例。这也是可以料想到的结果,所谓的“集合”与其他的引用类型相比,并没有什么本质的特殊性。至于为何一般此问题讨论的都是集合的特殊性,以及MSDN上也特别说明是“集合类型”,我想可能是因为DependencyProperty常用的是一些基本的值类型,String虽是引用类型,却是Immutable的,因此也不会出现这个问题;那么要用到引用类型而又可能出问题的,最常见的就是集合类型了,因此MSDN要特别把这个问题拿出来说明一下。

---------------------

好,那现在问题变得一般化了:一般的引用类型,为何会存在此“陷阱”?

我们还需要从DP的原理说起。DP必须定义为静态成员,其本质是静态的哈希表(当然其实际实现要复杂得多,至于为什么这么实现,微软有很多这方面的介绍,比如可以支持资源、样式、动画、数据绑定等等)。而类的每个实例在这张静态表中就有一项,用来记录每个实例对应DP的值,这个值会使用DP的PropertyMetadata所指定的defaultValue进行初始化。我们使用SetValue和GetValue方法,其实就是在对这张表中的对应项进行读写。

说到这里,大家应该都已经明白问题所在了,值类型属性的值就是存储在哈希表中的,因此修改值类型属性不会影响其他实例的相同属性;而引用类型属性在哈希表中存储的只不过是一个引用,初始化为指向同一对象,当对该属性进行修改时(比如向集合中添加元素),其实是向所有实例所共享的对象中添加元素,因此,其他实例的属性也会受到影响。

---------------------

也许还有人会问,为什么微软不为每个实例初始化一个新的引用对象呢?不错,我也这么想过,但正如我前面所说,按照现在的设计,微软不一定是没有想到,很可能是做不到。实际上,在PropertyMetadata对象初始化之前,引用对象已经创建完成了:

new PropertyMetadata (new List<int> ())

思考一下这段代码的运行顺序,在调用PropertyMetadata的构造函数之前,List<int>对象已经构造完成,因此PropertyMetadata拿到的就只是一个引用,它无法知道如何去构造这个引用对象,因此无法为哈希表每项都创建一个新的实例,而只能老老实实地使用手里这个引用。

因此,对于初始值为引用类型的DependencyProperty来说,我们确实只能在类的构造方法中对齐初始化(虽然微软建议使用PropertyMetadata初始化)。

 

Bars = new List<int> ();

等等,别以为这就完了,这里使用了Bars属性,实际上是调用了SetValue方法。那对于readonly的DependencyProperty呢?事实上,在使用集合时,很明显这个属性本身最好是只读的,我们修改的是集合的元素,而不是集合这个属性本身。在WPF中,定义一个只读的DependencyProperty的方法可以参考这里(SL 4不支持RegisterReadOnly方法,可以自己实现ReadOnly的效果,参考这篇博客),代码如下:

public List<int> Bars

{

    get { return (List<int>)GetValue (barsPropertyKey.DependencyProperty); }

}



private static readonly DependencyPropertyKey barsPropertyKey =

    DependencyProperty.RegisterReadOnly (

        "Bars",

        typeof (List<int>),

        typeof (Foo),

        new PropertyMetadata (null));

此时,使用了私有的DependencyPropertyKey类型字段代替了原来公开的DependencyProperty类型字段,Bars属性也去掉了set方法。那要如何进行初始化呢?

这时,SetValue方法的另一个重载就用上了,这个重载接受一个DependencyPropertyKey而不是一个DependencyProperty,而这正是RegisterOnly方法的返回值。(我原来一直奇怪为什么GetValue只有一个版本而SetValue有两个版本…)

代码如下:

public Foo ()

{

    SetValue (barsPropertyKey, new List<int> ());

}

 

---------------------

现在回到原来的问题上。

对于这样一个“陷阱”,难道真的没有办法?难道必须小心地把初始化放到类实例的构造函数中?

Prism框架中,当我们向一个Region注册View时,可以使用IRegionManager的扩展方法RegisterViewWithRegion:

public static IRegionManager RegisterViewWithRegion (

    this IRegionManager regionManager, 

    string regionName, 

    Func<object> getContentDelegate);

第三个参数,本应传入要注册的View对象,却传入了一个Func<object>,这是什么意思?我们再看一下实际使用代码:

_regionManager.RegisterViewWithRegion (RegionNames.SidebarRegion,

    () => _container.Resolve<ISidebarPresenter> ().View);

可见,实际传入的是一个匿名委托,这个委托告诉了RegionManager如何创建一个View。RegionManager得到这个委托后,用一个Dictinoary把它保存起来,到实际创建View的时候才去调用。因此_container.Resolve<ISidebarPresenter> ().View这段代码要到实际创建View时才会运行。同时,RegionManager只需反复调用委托,也具备了重复创建多个View实例的能力。

---------------------

这样的方法,也许WPF/SL会借鉴一下?或许某一天,我们可以写出这样的代码:

 

class Foo : DependencyObject

{

    public List<int> Bars

    {

        get { return (List<int>)GetValue (BarsProperty); }

        set { SetValue (BarsProperty, value); }

    }



    public static readonly DependencyProperty BarsProperty =

        DependencyProperty.Register (

            "Bars",

            typeof (List<int>),

            typeof (Foo),

            new PropertyMetadata (() => new List<int> ()));

}

而不用再去担心这样的“陷阱”。

你可能感兴趣的:(silverlight)