• 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> ()));
    }

    使用代码如下:

    76e073b4-59e4-4e46-b97e-3445c93aea76

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

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

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

    image

    果然,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进行初始化。调试结果如下:

    image

    可见,在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> ()));
    }

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

  • 相关阅读:
    关于word开发中字体大小
    WPF学习笔记
    C#各种配置文件使用,操作方法总结
    web.config和app.config使用
    微软 WordXML格式初步分析
    面向对象—C#高级编程(第10版)学习笔记8
    C#编程的推荐规则和约定—C#高级编程(第10版)学习笔记7
    C#基础—C#高级编程(第10版)学习笔记6
    .Net 应用程序体系结构—C#高级编程(第10版)学习笔记5
    通俗易懂说编程:.Net Core是什么、有何用?
  • 原文地址:https://www.cnblogs.com/shenfengok/p/2184800.html
Copyright © 2020-2023  润新知