WPF教程四:字段、属性、依赖项属性的演变过程

这个章节主要讲解属性是什么,为什么会演变出依赖项属性,依赖属性的优势是什么。以及如何更好的使用属性和依赖项属性。

一、属性

属性是什么。
  翻了好几本C#的书和微软的文档,我觉得对属性讲解比较好理解的就是《深入浅出WPF》这本书中关于属性的描述。照抄如下:
  程序的本质是“数据+算法”,用算法来处理数据以期得到输出结果。数据表现为各种各样的变量,算法表现为各种各样的函数。在面向对象的思维下即使有了类等数据结构,依然没有改变这个本质,类的作用只是把散落在程序中的变量和函数进行归档封装并控制对它们的访问。被封装在类中的变量成为字段。它表示类或实例的状态,封装在类中的函数成为方法。它标识类或实例的功能,这个时候的面向对象概念中还不包含事件、属性等概念。
  我们可以使用private、public 等修饰符来控制字段或方法的可访问性;是否使用static关键字来修饰字段或方法则决定了字段或方法是对类有意义还是对类的实例有意义。所谓“对类有意义”或“对实例有意义”都是语义范畴的概念。比如对于Human(人类)这个类来说。Weight(重量)这个字段对于人类的个体是有意义的,而对于“人类”这个概念并没有什么意义;Amount(总量)这个字段就不一样了。它对人类的个体没有意义,但对于人类是有意义的。方法也有类似的情况,比如Speak(说话)这个方法,只有人类的个体才能Speak,而Populate(繁衍)这个方法似乎对于人类比对于人类的个体更有意义。为了让程序满足语义要求C#语言规定:对类有意义的字段和方法使用static关键字修饰、称为静态成员,通过类名加访问操作符(.)可以访问它们,对类的实例有意义的字段和方法不加static关键字,称为非静态成员或实例成员。

  从语义方面来看,静态成员与非静态成员有着很好的对称性,但从程序在内存中的结构来看,这种对称就被打破了。静态字段在内存中只有一个拷贝非静态字段则是每个实例拥有一个拷贝方法也就是我们的函数无论是否为静态,在内存中只会有一份拷贝,区别只是你通过类名来访问存放指令的内存还是通过实例名来访问存放指令的内存。

  现在让我们看看属性是怎么演变出来的。字段被封装在实例中,要么能被外界访问(非使用private),要么不能(使用private)如图:

我们模拟一下实例+字段的写代码方式。

如上所示,直接给实例的字段赋值:假设下面的代码中singer._age = 11000是用户输入年龄。因为没有对应的字段检查,直接给这字段赋值非常不安全。年龄可能就赋值错了。

namespace ProperyAndDependencyPropertyExample
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var singer = new Human("山下直人", 15);
            //直接给对象的字段赋值。
            singer._name = "山下直人";
            //因为没有对应的检查,直接给这字段赋值非常不安全。年龄可能就赋值错了。
            singer._age = 11000;

        }
    }

    public class Human
    {
        /// <summary>
        /// 第一个字段、我们假设是名字。可以被外界访问。
        /// </summary>
        public string _name;
        /// <summary>
        /// 第二个字段,我们假设年龄.可以被外界访问。
        /// </summary>
        public int _age;
        /// <summary>
        /// 
        /// </summary>
        /// <param name="name">歌手名称</param>
        /// <param name="age">歌手年龄</param>
        public Human(string name, int age)
        {
            _name = name;
            _age = age;
        }

    }

}

我们在当前基础上添加一个变量用于在业务逻辑处理部分验证输入的年龄。修改部分代码如下:

  public MainWindow()
        {
            InitializeComponent();
            var singer = new Human("山下直人", 15);
            //直接给对象的字段赋值。
            singer._name = "山下直人";
              //用一个变量来接收输入的年龄,我们假设是11000
            var ageValue = 11000;
            //在业务处理部分添加对待写入字段的验证。     
            if (ageValue >= 0 && ageValue <= 150)
            {
                singer._age = ageValue;
            }
            else
            {
                throw new OverflowException("Age overflow");
            } 
        }

  第一种直接把数据暴露给外界的做法很不安全,很容易就会把错误的值写入字段。第二种在每次写入数据的时候都先判断一下值得有效性优会增加冗余代码并且违反了面向对象要求“高内聚”得原则,我们希望对象自己有能力判断将被写入得值是否正确,于是,程序员仍然把字段标记为private,但是使用一对非private得方法来包装它。在这对方法中,一个以Set为前缀且负责判断数据的有效性并写入数据,另外一个以Get为前缀且负责把字段里得数据读取出来。而如果我们缺少了Set方法的话。我们所定义的字段就会变成只读的。因为在类中定义的这个字段没有修改值得入口。

我们重新定义一个新的Human2的类,然后再初始化实例后使用SetAge赋值来演示这个封装过程。这样我们就完整的完成了对数据的封装。而我们整个封装过程使用了函数,所以再程序中无论有多少实例都只有一处。

  public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var singer2 = new Human2();
            singer2.SetAge(11100);

        }
    }

public class Human2
    {
        private int _age;
        public void SetAge(int value)
        {
            if (value >= 0 && value <= 150)
            {
                this._age = value;
            }
            else
            {
                throw new OverflowException("Age overflow");
            }
        }

        public int GetAge()
        {
            return this._age;
        }

    }

 c#语法中对封装的整个过程(Get、Set)进一步的合并成了属性代码就可以变成如下这样:

 public class Human3
    {
        private int _age;
        public int Age
        {
            get { return _age; }
            set
            {
                if (value >= 0 && value <= 150)
                {
                    this._age = value;
                }
                else
                {
                    throw new OverflowException("Age overflow");
                }
            }
        }
    }

整个过程是不是很熟悉,这不就是天天在C#面试当中背的C#的三大特性,封装、继承、多态中的封装吗?值类型和引用类型的相互转换。

因为有私有字段,所以初始化时,就申请了私有字段的内存,并赋了初始值。在WPF中微软将属性整个概念又推进了一步,推出了依赖属性这个概念。下面开始讲解依赖项属性啦!!!

二、依赖项属性

重点来啦!!!终于写到依赖项属性啦!!!从教程一开始就在等这一章节啦!!!

有很多大佬的博客都已经基于源码讲了什么是依赖项属性,依赖项属性是怎么工作的。但是我经过了很久的时间,依然没有用好依赖项属性、没有理解依赖项属性,因为很多时候我都在直接用各种Template+属性+INotifyPropertyChanged,来实现我想要的东西,哪里适合依赖项属性,如何更好的使用依赖项属性。我很少去关心这个。所以这次也是从头来梳理依赖项属性。

减少废话,开搞。

   我们刚才我们讲解了什么是字段、什么是属性,我们知道了属性都包装着一个非静态的字段,属性的非静态字段会在实例初始化过程中分配内存,而每一个实例都有自己的属性,这样的话。你有多少个实例、你就有多少个初始化的字段。即使这些被实例化的对象中的字段你可能永远不会去用它。我们假设每个属性都包装了一个4字节的字段。我们假设在一个邮件客户端中,默认有1W封未读的邮件(日积月累的垃圾邮件),我们的邮件List在拿到数据初始化中包含了发件人,收件人,时间、标题、内容等等算10项的话,假设都是使用Textblock显示这些字段邮件列表的时候占用的字段约等于4(每个字段的初始化字节)*10000(邮件数量)*10(没封邮件有10个属性)=400000 字节=0.38M内存,这中间还没有包含Textblock控件中其他属性字段初始化使用的内存,也就是说即使使用列表虚拟化和数据虚拟化技术。来提高响应,降低内存。初始化字段的的这些内存,是无法省略的,即使某些字段可能永远不会使用。

在传统的NET开发中,一个对象所占用的内存空间在创建实例化的时候就已经决定了。而WPF允许对象在实例化的时候不包含字段占用的空间,也可以从其他对象获取需要用到的值,这种能力需要使用依赖对象来实现。而依赖项属性的宿主就是依赖对象(DependencyOObject)。

我们来分析一下依赖项属性,我从一个TextBlock控件F12查找父级。找到DependencyObject对象,DependencyObject继承自DispatcherObject。这是一个线程调度用的类。所以我们只关注到DependencyObject。

  ,

 我们从上图看他的继承关系,建议每个都看看。这样就能看出来整体WPF的设计。里面都包含了什么等等。但是今天的重点是依赖项属性。 我们看到DependencyObject是处理依赖项属性的部分,也就是说所有的子对象都支持依赖项属性。也就是说我们

 

 有兴趣的可以看看GetValue和SetValue源码怎么实现的。但是这里不讲。这里只分析,依赖项属性我们应该怎么用更好。从上面的2个截图可以看出来所有的UI控件都是依赖项对象。UI控件中根据定义只要是DependencyProperty类型的都是依赖项属性,命名都是Property结尾的。而使用同名不带Property的属性使用GetValue和SetValue来封装依赖项属性。

我们记得刚才说的依赖项属性相对于之前属性的优势,不用每次在实例化的时候初始化实例对象的字段可以从其他对象获取需要的值

我们来写个例子:

我们创建一个类Student 继承自DependencyObject 作为宿主,使用propdp+2次tab快捷方式来创建依赖项属性

 修改代码如下,这样我们就创建好了一个包含依赖项属性的Student类了,在回忆一次依赖项属性的优势,及使用它的优势是什么?不用每次在实例化的时候初始化实例对象的字段,可以从其他对象获取需要的值

 public class Student : DependencyObject
    {  
        public string Name
        {
            get { return (string)GetValue(NamePropertyProperty); }
            set { SetValue(NamePropertyProperty, value); }
        }

        // Using a DependencyProperty as the backing store for NameProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty NamePropertyProperty =
            DependencyProperty.Register("Name", typeof(string), typeof(Student));
    }

按照刚才属性中讲得,方法只有一份拷贝,我们创建的NamePropertyProperty是的DependencyProperty类型的属性。 注意这个DependencyProperty.Register。结合上面的代码,就是我们注册了一个名字叫做NameProperty的属性,他是string类型,属于Student类。我们这里不追究原理和如何实现,只注重如何使用和使用场景。

XAML代码如下:

<Window x:Class="ProperyAndDependencyPropertyExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ProperyAndDependencyPropertyExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>

            <TextBlock  Text="F12看下依赖项属性的结构"/>
            <TextBox x:Name="tb_text1"  Text=""/>
            <TextBox x:Name="tb_text2" Text=""/>
            <Button Content="读取依赖项属性" Click="GetDependencyProperty_Click"/>
        </StackPanel>
    </Grid>
</Window>

     //Student是继承自DependencyObject的宿主对象
        Student stu = new Student();
        private void GetDependencyProperty_Click(object sender, RoutedEventArgs e)
        { 
            //NamePropertyProperty是我们的依赖项属性。
            stu.SetValue(Student.NamePropertyProperty, this.tb_text1.Text);
            tb_text2.Text = (string)stu.GetValue(Student.NamePropertyProperty);
        }

通过以上代码。我们创建了一个依赖属性的宿主类Student,创建了依赖项属性NameProperty。

我们在点击Click按钮的时候实例化了宿主对象,并设置了宿主对象的NameProperty依赖项属性的值等于我们输入的第一个textBox的值。

然后让第二个textBox的Text显示值等于宿主对象下的依赖项属性的值。 效果图如下:

 我们使用内存测试工具来分析当前内存分配情况

  我们通过分析软件截取了点击按钮之前和点击按钮之后的内存分配情况,在依赖项属性在我们点下Button后。我们的内存并没有增长。因为这2个textbox使用GetValue和SetValue获取或设置同一个对象的依赖项属性的值。我不知道这样讲是否合理,但是我主要表达的思想就是依赖项属性和属性的用法相差不大。而且更节省内存。而且我们在上面创建的Name就是对于依赖项属性的Get和Set封装。有了这个属性之后,我们就可以通过修改Name直接使用了。 在Binding章节普通的属性需要实现INotifyPropertyChanged的变更消息才能实现属性变更推送,但是依赖项属性已经默认实现了这个功能。

 //Student是继承自DependencyObject的宿主对象
        Student stu = new Student();
        private void GetDependencyProperty_Click(object sender, RoutedEventArgs e)
        {
            //NamePropertyProperty是我们的依赖项属性。
            stu.SetValue(Student.NamePropertyProperty, this.tb_text1.Text);
            tb_text2.Text = (string)stu.GetValue(Student.NamePropertyProperty);
            //上面的代码不好理解,但是通过封装之后下面的调用就比较好使用和理解了。
            //通过继承关系我们知道了所有的UI控件都拥有依赖项属性,主要也是设计UI层控件的。
            //我们在设计控件的时候就尽量考虑和使用依赖项属性,使用的多了就熟悉了。
            stu.Name = this.tb_text1.Text;
            this.tb_text2.Text = stu.Name;
        }

 依赖项属性就讲到这里,关于数据存储到了什么位置,这个DependencyProperty.Register是怎么在工作的。希望大家去读源码。加深对依赖属性的认知,毕竟依赖项属性我的理解,他是属性的升级版,是一个很不错的设计。但是WPF如果深入进去的话。你会发现很多设计都特别棒。还是建议去读源码。依赖项属性就写到这里。 你可以理解为更节省内存的属性并有属性变更通知的功能,如果使用属性封装依赖项属性的getValue和SetValue的话。就会更方便的使用。

我创建了一个C#相关的交流群。用于分享学习资料和讨论问题。欢迎有兴趣的小伙伴:QQ群:542633085

原文地址:https://www.cnblogs.com/duwenlong/p/14525896.html