WPF学习(二) - 绑定

    绑定,这个看起来很神奇的东西,于我这种喜欢刨根儿的人而言,理解起来非常困难。
    WPF绑定的核心思想是:数据层属性值的改变,能反应给展示层,反之亦然,并且这个响应的过程能被分离出来。


    传统Winform编程更加原始,没有那么多隐藏的(implicate)技术。我就以winform的实现方式来领会WPF的机制。

public class DataLayer
    {
        public delegate void TextChangedEventHandler ( object sender, EventArgs e );

        public event TextChangedEventHandler TextChanged;

        private string text = "";

        public string Text
        {
            get { return text; }
            set
            {
                if ( text != value )
                {
                    text = value;
                    if ( this.TextChanged != null )
                        this.TextChanged ( this, new EventArgs ( ) );
                }
            }
        }

    }
数据层代码
public partial class PresentationLayer : Form
    {
        TextBox ui = new TextBox ( );
        DataLayer data = new DataLayer ( );

        public PresentationLayer ( )
        {
            InitializeComponent ( );
            this.Controls.Add ( ui );

            ui.TextChanged += new System.EventHandler ( this.PresentationLayerTextChanged );
            data.TextChanged += new DataLayer.TextChangedEventHandler ( this.DataLayerTextChanged );
        }

        private void PresentationLayerTextChanged ( object sender, EventArgs e )
        {
            data.Text = ui.Text;
        }

        private void DataLayerTextChanged ( object sender, EventArgs e )
        {
            ui.Text = data.Text;
        }

    }
展示层代码

这样就实现了前、后台数据的同步。缺点有三方面:

  1、每个属性的数据同步,功能单一,内容相同。没有必要在数据层对每个属性的响应事件都单独定义事件委托。
  2、为了保持数据同步,需要在展示层编写大量的数据同步代码。如果有很多个属性,重复的工作量非常大。
  3、data作为PresentationLayer的成员,增加了耦合度。数据层和展示层没有完全分隔开。

  问题1很好解决,从数据层抽象出一个接口事件,并在事件参数中要求说明激发事件的属性名

    public interface INotifyPropertyChanged
    {
        event PropertyChangedEventHandler PropertyChanged;
    }

    public delegate void PropertyChangedEventHandler ( object sender, PropertyChangedEventArgs e );

    public class PropertyChangedEventArgs : EventArgs
    {
        private readonly string propertyName;

        public PropertyChangedEventArgs ( string propertyName )
        {
            this.propertyName = propertyName;
        }

        public virtual string PropertyName
        {
            get
            {
                return this.propertyName;
            }
        }
    }
抽象出来的接口事件

这样,原始的DataLayer就变成这样

    public class DataLayerExt : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string text = "";

        public string Text
        {
            get { return text; }
            set
            {
                if ( text != value )
                {
                    text = value;
                    if ( this.PropertyChanged != null )
                        this.PropertyChanged ( this, new PropertyChangedEventArgs ( "Text" ) );
                }
            }
        }

    }
DataLayerExt

  问题2从逻辑上也不难解决,定义一个静态的方法,方法的参数要说明是哪两个对象的哪两个属性需要同步,并记录这种同步关系

    public class BindingOperations
    {
        static List<BindingRelative> lstBindingRelative = new List<BindingRelative>();

        // 源和目标的类型应该是object。这里只为表达语意,完全实现很困难
        public static void SetBinding ( TextBox uiObject, string uiPropertyName, DataLayerExt dataObject, string dataPropertyName )
        {
            // 用列表记录同步关系
            lstBindingRelative.Add ( new BindingRelative ( ) { UIObject = uiObject, UIObjectPropertyName = uiPropertyName, DataObject = dataObject, DataObjectPropertyName = dataPropertyName } );
            // 增加事件处理方法
            uiObject.TextChanged += new EventHandler ( uiObject_TextChanged );
            dataObject.PropertyChanged += new PropertyChangedEventHandler ( target_PropertyChanged );
        }

        static void uiObject_TextChanged ( object sender, EventArgs e )
        {
            foreach ( BindingRelative item in lstBindingRelative )
            {
                if ( item.UIObjectPropertyName == "Text" )
                {
                    // 通用的方式应该是这样
                    //item.Source.SourcePropertyName = item.Target.dataPropertyName;
                    item.DataObject.Text = item.UIObject.Text;
                    break;
                }
            }
        }

        static void target_PropertyChanged ( object sender, PropertyChangedEventArgs e )
        {
            foreach ( BindingRelative item in lstBindingRelative )
            {
                if ( item.DataObjectPropertyName == e.PropertyName )
                {
                    // 通用的方式应该是这样
                    //item.Source.SourcePropertyName = item.Target.dataPropertyName;
                    item.UIObject.Text = item.DataObject.Text;
                    break;
                }
            }

        }
    }

    //定义一个用来记录绑定关系的结构
    public class BindingRelative
    {
        public TextBox UIObject;
        public string UIObjectPropertyName;
        public DataLayerExt DataObject;
        public string DataObjectPropertyName;
    }
建立数据同步关系

  这样,展示层只需要设计界面的风格,定义界面中的展示元素,与后台数据完全分离。不知不觉中,问题1也一并解决了。

    public partial class PresentationLayerExt : Form
    {

        public TextBox ui = new TextBox ( );

        public PresentationLayerExt ( )
        {
            InitializeComponent ( );
            this.Controls.Add ( ui );
        }

    }
PresentationLayerExt

  当然,指定同步关系还是要用代码实现的,但不在展示层,而是在外部,比如在Main()函数处

    static class Program
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main ( )
        {
            Application.EnableVisualStyles ( );
            Application.SetCompatibleTextRenderingDefault ( false );

            PresentationLayerExt p = new PresentationLayerExt ( );
            DataLayerExt d = new DataLayerExt ( );
            BindingOperations.SetBinding ( p.ui, "Text", d, "Text" );

            Application.Run ( p );
        }
    }
Main()

  以上,用Winform实现了数据同步,并做到了数据层与展示层的分离。由于Winform中根据属性名称字符串获取对象的属性非常困难,定义同步关系的方法SetBinding有很大的缺陷,几乎没有通用性可言。但这不妨碍通过Winform的示例理解WPF绑定技术的运行机制。

  WPF的绑定技术,实现数据层和展示层的数据同步原理也是这样:
    数据层的类需要派生自System.ComponentModel中的INotifyPropertyChanged接口,
    每个类都必须包含PropertyChanged事件,并在属性值改变时,激发这个事件,事件参数中传入属性名。

    数据同步关系由BindingOperations的SetBinding方法建立。

    在WPF中SetBinding方法的后两个参数打包成Binding类型的对象

    这样,SetBinding方法的对象参数就可以使用最原始的DependencyObject类型的对象,提高通用性。
    很多WPF控件已封装过SetBinding方法,这样就在对象绑定自己的属性时,就可以调用自己的SetBinding方法省略目标这个参数

  一个完整的WPF应用Binding技术的例子

    // Data作为绑定的数据源头,总会接收绑定目标的改变
    // 但是,如果想将Data源头的改变告诉绑定目标,
    // 必须要从INotifyPropertyChanged接口派生,并实现响应事件的激发
    public class DataClass : System.ComponentModel.INotifyPropertyChanged
    {
        private string myString;

        public string MyString
        {
            get { return myString; }
            set
            {
                myString = value;
                
                //激发事件
                if ( PropertyChanged != null )
                    PropertyChanged ( this, new System.ComponentModel.PropertyChangedEventArgs ( "MyString" ) );
            }
        }

        public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    }
定义数据类

 

    // 作为被绑定的目标类,必须从DependencyObject派生
    // 这样定义的类才能满足SetBinding方法的第一个参数的类型要求
    // 还要额外定义一个依赖属性,用来满足SetBinding方法的第二个参数要求
    // 用DependencyObject派生方法GetValue和SetValue,控制属性的存、取
    public class UIClass : System.Windows.DependencyObject
    {
        public string MyText
        {
            get { return ( string ) GetValue ( MyTextProperty ); }
            set { SetValue ( MyTextProperty, value ); }
        }

        public static readonly System.Windows.DependencyProperty MyTextProperty =
            System.Windows.DependencyProperty.Register ( "MyText", typeof ( string ), typeof ( UIClass ) );

    }
展示类

 

测试类

 

  相比Winform,WPF实现了按属性名称存/取对象属性的功能,并把这个技术叫做依赖属性。

  有了依赖属性,WPF的SetBinding方法就真正做到通用,这个技术在后面继续研究。

 

  程序的世界哪有什么神奇,只是有的人做了更多的工作,结果看起来很神奇而已。

  所谓的数据与展示分离,不过是在这两层之外,额外创建了一个管理机构。

  而WPF的绑定技术,就是这个管理机构中的一个部门,负责收、发快递!

posted @ 2015-06-20 16:13  规定  阅读(540)  评论(0编辑  收藏  举报