c# 模仿 vue 实现 winform 的数据模型双向绑定
2017-11-30 21:19
921 查看
前前前段时间面试遭拒,当时面试关问自己的一些东西确实不懂,其中就包括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
。为统一通知(观察者调用),我们声明一个接口:
然后实现这个接口(详细看注释)
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
这样就行了么?
当然不行,我们还要给对象的属性手动改get set 呢(wpf也是要手动给需要绑定通知变更的属性改get set),前面Dep我们集成了 Get Set ,那这列给模型属性改 get set 就简单多了。
这就完了么? 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.最后,在窗口初始化时加上创建绑定对象。
设定好控件的 Tag,我们在窗体初始化的时候,是需要简单的一行代码就可以了。
到这里就实现了。看效果(这里 输入框 和 label 以及 滚动条 绑定的是 Value 属性,而 按钮绑定 Text 属性 和 Click 事件)
但是自己怎么能轻易停留在用呢,于是在花了点时间,直接搜索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; /// < c8b6 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 事件)
相关文章推荐
- 应用defineProperty简单实现vue的双向数据绑定
- vue动态载入变量键值名,实现数据双向绑定
- c#winform datagridview的数据绑定和行的增删改实现
- Vue.js实现双向数据绑定(表单自动赋值、表单自动取值)
- vue.js利用defineProperty实现数据的双向绑定
- Vue实现双向数据绑定
- Vue 2.0学习笔记:实现组件数据的双向绑定
- 如何使数据绑定的控件实现“上一条”、“下一条”、“第一条”和“最后一条”的功能?(WinForm, C#)
- vue父组件和子组件通过sync实现双向数据绑定
- ASP.NET Web API实践系列07,获取数据, 使用Ninject实现依赖倒置,使用Knockout实现页面元素和视图模型的双向绑定
- VUE-Table上绑定Input通过render实现双向绑定数据
- Angular和Vue双向数据绑定的实现原理(重点是vue的双向绑定)
- vue双向数据绑定的实现原理
- 【VUE】.NET实现Tree组件双向绑定数据(1)
- vue实现数据双向绑定的原理
- Vue实现双向绑定的原理以及响应式数据
- 【VUE】.NET实现Tree组件双向绑定数据(2)
- Angular和Vue双向数据绑定的实现原理(重点是vue的双向绑定)
- vue.js双向数据绑定实现原理
- 浅谈vue中数据双向绑定的实现原理