1. 程式人生 > >c# 模仿 vue 實現 winform 的資料模型雙向繫結

c# 模仿 vue 實現 winform 的資料模型雙向繫結

前前前段時間面試遭拒,當時面試關問自己的一些東西確實不懂,其中就包括vue(其實有看過相關文章和文件,秉著 如果只是用輪子的話,需要時間和文件就夠了,事實上只是使用的話,按規範來就行了)。

但是自己怎麼能輕易停留在用呢,於是在花了點時間,直接搜尋vue繫結原理,詳細看了兩篇文章 

http://m.jb51.net/article/107927.htm

http://www.cnblogs.com/likeFlyingFish/p/6744212.html  其實就是觀察者的設計模式,關鍵在於事件的監聽。而 js 有個 defineProperty 修改屬相的get set 方法,使其在 set 資料的時候判斷是否有變化後進行通知,也就是執行一邊對該屬性訂閱的方法。 再將我們平時對dom操作的方法進行封裝註冊到某個屬性的訂閱列表,即可實現資料檢視繫結。其實不管怎麼做,最終也還是要操作dom的,只是人家封裝好了的而已。 而後面突然有個想法,我能不能模仿 vue 做個winform 的雙向繫結呢? 儘管視窗開發 雙向繫結已經有成熟的 wpf 了, 而 winform 也有 DataBindings 繫結資料(這個在後面自己的測試中發現沒能在變化中通知,應該還要進一步的封裝),但是就想把 vue的方法用c#實現一下。 按照 js 那一套,要有個 Observe 來重寫物件屬性的 get;set;,還要有個 Dep 來儲存訂閱者,還要有個 Watcher 來監聽屬性。 但是 c# 沒法程式碼批量改 getter setter 這兩個方法(試過反射不出這兩個方法),那隻需要實現 Dep 和Watcher 。
Watcher 物件的屬性通過Watcher 來監聽,不同控制元件繫結同一個 屬性的話,就是不同的Watcher 。為統一通知(觀察者呼叫),我們宣告一個介面:
namespace Mvvm
{
    /// <summary>
    /// 統一監聽介面 
    /// </summary>
    public interface IWatcher
    {
        void Update();
    }
}

然後實現這個介面(詳細看註釋)
namespace Mvvm
{
    /// <summary>
    /// 監聽者
    /// </summary>
    public class Watcher : IWatcher
    {
        /// <summary>
        /// 實體型別
        /// </summary>
        private Type type = null;
        /// <summary>
        /// 屬性變化觸發的委託
        /// </summary>
        private Action<object> Action = null;
        /// <summary>
        /// 屬性名稱
        /// </summary>
        private string propertyName = null;
        /// <summary>
        /// 父控制元件
        /// </summary>
        private Control ParentControl = null;
        /// <summary>
        /// 實體
        /// </summary>
        private object model = null;
        /// <summary>
        /// 初始化監聽者
        /// </summary>
        /// <param name="ParentControl">父控制元件</param>
        /// <param name="type">實體型別</param>
        /// <param name="model">實體</param>
        /// <param name="propertyName">要監聽的屬性名稱</param>
        /// <param name="action">屬性變化觸發的委託</param>
        public Watcher(Control ParentControl, Type type, object model, string propertyName, Action<object> action)
        {
            this.type = type;
            this.Action = action;
            this.propertyName = propertyName;
            this.ParentControl = ParentControl;
            this.model = model;
            this.AddToDep();
        }
        /// <summary>
        /// 新增監聽者到屬性的訂閱者列表(Dep)
        /// </summary>
        private void AddToDep()
        {
            PropertyInfo property = this.type.GetProperty(this.propertyName);
            if (property == null) return;
            Dep.Target = this;
            object value = property.GetValue(this.model, null);
            Dep.Target = null;
        }
        /// <summary>
        /// 更新資料(監聽觸發的方法)
        /// </summary>
        public void Update()
        {
            this.ParentControl.Invoke(new Action(delegate
            {
                this.Action(this.model);
            }));
        }
    }
}
這裡的 ParentControl 指的是 From 或者是 包含有子空間的其它容器,繫結的話只會繫結其下的子控制元件,同時,非同步更新UI時也是由該控制元件的委託來完成.

Dep

Dep 主要用來儲存某個屬性的訂閱者,以便屬性值變更時,通知其訂閱者。因為 c#沒法像js 一樣可以用程式碼批量重寫get set,所以為了簡化屬性get set 的寫法,將屬相的值也存放到 Dep 裡(每個屬性對應單獨的 Dep),通過Dep 中的Get Set 方法給屬性操作。

namespace Mvvm
{
    public class Dep
    {
        /// <summary>
        /// 全域性變數,使用者將指定屬性的訂閱者放入通知列表
        /// </summary>
        public static IWatcher Target = null;
        /// <summary>
        /// 儲存屬性的值,屬性的get set將是對該值的操作
        /// </summary>
        private object oValue;
        /// <summary>
        /// 訂閱者列表
        /// </summary>
        private List<IWatcher> lsWatcher = null;



        public Dep()
        {
            this.lsWatcher = new List<IWatcher>();
        }
        /// <summary>
        /// 新增訂閱者
        /// </summary>
        /// <param name="watcher"></param>
        private void PushWatcher(IWatcher watcher)
        {
            this.lsWatcher.Add(watcher);
        }
        /// <summary>
        /// 通知
        /// </summary>
        public void Notify()
        {
            List<IWatcher> watchers = this.lsWatcher;
            watchers.ForEach(watcher =>
            {
                watcher.Update();
            });
        }
        /// <summary>
        /// 返回屬性的值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T Get<T>()
        {
            // Dep.Target 不為空時,標識指向這個屬性的一個訂閱者,需要將它加入到訂閱列表
            bool flag = Dep.Target != null;
            if (flag)
            {
                this.PushWatcher(Dep.Target);
            }
            return this.oValue==null?default(T): (T)this.oValue;
        }
        /// <summary>
        /// 設定屬性值
        /// </summary>
        /// <param name="oValue"></param>
        public void Set(object oValue)
        {
            bool flag = this.oValue == null || !this.oValue.Equals(oValue);
            if (flag)
            {
                this.oValue = oValue;
                this.Notify();
            }
        }
        /// <summary>
        /// 初始化佇列,分配給每個屬性
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        public static List<Dep> InitDeps(int count)
        {
            if (count < 1) throw new Exception("wrong number! count must biger than 0");
            var lsDep = new List<Dep>();
            for(var i=0;i<count;i++)
            {
                lsDep.Add(new Dep());
            }
            return lsDep;
        }
    }

}

到目前為止,已經能手動新建 Watcher來實現 屬相變化 並觸發事件,但是,還要手動寫觸發事件,這怎麼能忍。於是進行了一層封裝,讓繫結自動化,預設對幾個變化事件做了繫結。

ViewBind
namespace Mvvm
{
    public class ViewBind
    {
        /// <summary>
        /// 預設繫結事件
        /// </summary>
        private string Events = "CollectionChange|SelectedValueChanged|ValueChanged|TextChanged";
        //private string Perpertis = "DataSource|Value|Text";

        /// <summary>
        /// 繫結檢視
        /// </summary>
        /// <param name="ParentControl">父控制元件</param>
        /// <param name="model">模型(物件)</param>
        public ViewBind(Control ParentControl, object model)
        {
            this.BindingParentControl(ParentControl, model);
        }
        /// <summary>
        /// 繫結控制元件
        /// </summary>
        /// <param name="ParentControl">父控制元件</param>
        /// <param name="model">實體</param>
        private void BindingParentControl(Control ParentControl, object model)
        {
            this.BindControl(ParentControl, model, ParentControl.Controls);
        }
        /// <summary>
        /// 繫結控制元件
        /// </summary>
        /// <param name="ParentControl">父控制元件</param>
        /// <param name="model">實體</param>
        /// <param name="Controls">子控制元件列表</param>
        private void BindControl(Control ParentControl, object model, Control.ControlCollection Controls)
        {
            foreach (Control control in Controls)
            {
                var tag = control.Tag;
                if (tag == null) continue;
                foreach (var tagInfo in tag.ToString().Split('|'))
                {
                    var tagInfoArr = tagInfo.Split('-');
                    if (tagInfoArr[0].Equals("data") && tagInfoArr.Length == 3)
                    {
                        //數目繫結
                        string propertyName = tagInfoArr[tagInfoArr.Length - 1];
                        this.BindingProperty(ParentControl, control, model, propertyName, tagInfoArr[1]);
                        this.BindListener(control, model, propertyName, tagInfoArr[1]);
                    }
                    else if (tagInfoArr[0].Equals("ev") && tagInfoArr.Length == 3)
                    {
                        //事件繫結
                        BindEvent(ParentControl, control, model, tagInfoArr[1], tagInfoArr[2]);
                    }
                    else
                    {
                        if (control.Controls.Count > 0)
                        {
                            this.BindControl(ParentControl, model, control.Controls);
                        }
                    }
                }

            }
        }
        /// <summary>
        /// 繫結事件
        /// </summary>
        /// <param name="ParentControl">父控制元件</param>
        /// <param name="control">要繫結事件的控制元件</param>
        /// <param name="model">實體</param>
        /// <param name="eventName">事件名稱</param>
        /// <param name="methodName">繫結到的方法</param>
        private void BindEvent(Control ParentControl, Control control, object model, string eventName, string methodName)
        {
            var Event = control.GetType().GetEvent(eventName);
            if (Event != null)
            {
                var methodInfo = model.GetType().GetMethod(methodName);
                if (methodInfo != null)
                {
                    Event.AddEventHandler(control, new EventHandler((s, e) =>
                    {
                        ParentControl.Invoke(new Action(() =>
                        {
                            switch (methodInfo.GetParameters().Count())
                            {
                                case 0: methodInfo.Invoke(model, null); break;
                                case 1: methodInfo.Invoke(model, new object[] { new { s = s, e = e } }); break;
                                case 2: methodInfo.Invoke(model, new object[] { s, e }); break;
                                default: break;
                            }
                        }));
                    }));
                }
            }
        }
        /// <summary>
        /// 新增監聽
        /// </summary>
        /// <param name="control">要監聽的控制元件</param>
        /// <param name="model">實體</param>
        /// <param name="mPropertyName">變化的實體屬性</param>
        /// <param name="controlPropertyName">對應變化的控制元件屬性</param>
        private void BindListener(Control control, object model, string mPropertyName, string controlPropertyName)
        {
            var property = model.GetType().GetProperty(mPropertyName);

            var events = this.Events.Split('|');
            foreach (var ev in events)
            {
                var Event = control.GetType().GetEvent(ev);
                if (Event != null)
                {
                    Event.AddEventHandler(control, new EventHandler((s, e) =>
                    {
                        try
                        {
                            var controlProperty = control.GetType().GetProperty(controlPropertyName);
                            if (controlProperty != null)
                            {
                                property.SetValue(model, Convert.ChangeType(controlProperty.GetValue(control, null), property.PropertyType), null);

                            }
                        }
                        catch (Exception ex)
                        {
                            //TPDO
                        }
                    }));
                }
            }
        }
        /// <summary>
        /// 繫結屬性
        /// </summary>
        /// <param name="ParentControl">父控制元件</param>
        /// <param name="control">繫結屬性的控制元件</param>
        /// <param name="model">實體</param>
        /// <param name="mPropertyName">繫結的實體屬性名稱</param>
        /// <param name="controlPropertyName">繫結到的控制元件的屬性名稱</param>
        private void BindingProperty(Control ParentControl, Control control, object model, string mPropertyName, string controlPropertyName)
        {

            Action<object> action = info =>
            {
                try
                {
                    var controlType = control.GetType();
                    var mType = info.GetType().GetProperty(mPropertyName);
                    var controlProperty = controlType.GetProperty(controlPropertyName);
                    if (controlProperty != null)
                    {
                        controlProperty.SetValue(control, Convert.ChangeType(mType.GetValue(info, null), controlProperty.PropertyType), null);
                    }

                }
                catch (Exception ex)
                {
                    //TODO
                }
            };
            //新增到監聽
            new Watcher(ParentControl, model.GetType(), model, mPropertyName, action);
            //初始化資料(將實體資料賦給控制元件屬性)
            action(model);
        }
    }
}

這樣就行了麼?

當然不行,我們還要給物件的屬性手動改get set 呢(wpf也是要手動給需要繫結通知變更的屬性改get set),前面Dep我們集成了 Get  Set  ,那這列給模型屬性改 get set 就簡單多了。

 public class TestModel
        {
            List<Dep> dep = Dep.InitDeps(3);
            public int Value { get=>dep[0].Get<int>(); set=>dep[0].Set(value); }
            public string Text { get=>dep[1].Get<string>(); set=>dep[1].Set(value); }
            public string BtnName { get=>dep[2].Get<string>(); set=>dep[2].Set(value); }

            public void BtnClick(object o)
            {
                this.BtnName = string.Format("繫結事件{0}", DateTime.Now.Millisecond);
            }
        }

這就完了麼? NO

就像 vue ,要正常解析,要按 它的規則去在程式碼中加標識 什麼 v-if  v-model 等等,那我們這裡也有個規則。說到這個規則,winform控制元件裡有個 Tag 屬性,是給程式設計師做拓展用的,一般很少用,而我們的 ViewBind解析程式碼裡面是按照一定規則解析的。

我們的規則是這樣的 

繫結屬性: data-控制元件要繫結的屬性名-模型對應的屬性名  ,比如給 控制元件的 Text 屬性繫結 模型物件 的 Name 屬性 則 data-Text-Name.

繫結事件:ev-控制元件的事件名-模型中的方法名   ,如控制元件的 Click 事件 繫結模型中 Change 方法 則 ev-Click-Change

一個控制元件繫結多個屬性或者事件用|隔開 ,如 data-Text-Name|ev-Click-Change

到這裡準備工作就做完了。

What???準備工作那麼多?

不不不,前面的 Dep ,Watcher 等只是封裝好了的,我們要做的就是 

1.更改我們 模型屬性的 get set 方法,看程式碼很清楚,很容易,只要建立多個 Dep,然後給每個 屬性改一下。

2.按照規則給控制元件繫結資料或者事件。

3.最後,在視窗初始化時加上建立繫結物件。

private void Form1_Load(object sender, EventArgs e)
        {
            var model = new TestModel { Value = 50, BtnName="繫結事件" };
            new ViewBind(this, model);
        }

設定好控制元件的 Tag,我們在窗體初始化的時候,是需要簡單的一行程式碼就可以了。

到這裡就實現了。看效果(這裡 輸入框 和 label 以及 滾動條 繫結的是 Value 屬性,而 按鈕繫結 Text 屬性 和 Click 事件)