winform两个控件数据双向绑定时需要注意的坑
简单例子
我们还是以简单需求举例。现在需要我们做一个圆形按钮的自定义控件,和一个拖动条,当我们拖动拖动条时,圆形按钮的直径大小会随着拖动条的指针位置变化,当我们在圆形按钮控件上鼠标左键点击,圆形按钮直径变大,右键点击,直径变小,同时拖动条的指针也会移动到对应的位置。
思路解析
首先,圆形按钮的自定义控件我们已经做过了,可以直接拿来用,具体可以直接看这篇文章。拖动条我们可以直接拿工具箱自带的trackbar。首先我们来看下圆形按钮控件的代码,为了专门说明本篇文章的主题,所以我特意做了一些改变。
public partial class CircleButton : Button { public delegate void OnDiameterChanged(int value); public event OnDiameterChanged DiameterChanged; private int diameter = 100; public int Diameter { get { return diameter; } set { diameter = value; SetDiameter(); DiameterChanged(diameter); } } public CircleButton() { InitializeComponent(); ClientSize = new Size(200, 200); FlatStyle = FlatStyle.Flat; FlatAppearance.BorderSize = 0; BackColor = Color.Red; SetDiameter(); } private void SetDiameter() { GraphicsPath grPath = new GraphicsPath(); grPath.AddEllipse(0, 0, diameter, diameter); Region = new Region(grPath); } }
可以看到,圆形按钮向外暴露了一个直径改变的事件,所以在主窗体中,我们可以给直径改变的事件订阅一个方法,这个方法里的逻辑就是根据直径大小改变trackbar的指针位置;然后对于trackbar,给它的valueChanged事件订阅一个方法,去改变圆形按钮的直径大小,这样,我们就实现了两个控件的双向绑定。那个鼠标左右键点击完全是为了验证是否是双向绑定。以下是实现代码:
public partial class Form1 : Form { private CircleButton cb; private TrackBar trackBar; public Form1() { InitializeComponent(); //先把圆形按钮和trackbar都添加到窗体中 cb = new CircleButton(); cb.Location = new Point(130, 130); this.Controls.Add(cb); trackBar = new TrackBar(); trackBar.Minimum = 0; trackBar.Maximum = 200; trackBar.Orientation = Orientation.Horizontal; trackBar.TickFrequency = 1; trackBar.Location = new Point(500, 150); trackBar.Width = 300; trackBar.Value = cb.Diameter; this.Controls.Add(trackBar); //直接使用lambda(匿名函数)订阅事件, //如果这个函数只被调用一次,那么lambda是非常好的选择 cb.DiameterChanged += (value) => { trackBar.Value = value; }; trackBar.ValueChanged += (sender, e) => { cb.Diameter = trackBar.Value; }; cb.MouseDown += (sender, e) => { if (e.Button == MouseButtons.Left) { cb.Diameter += 1; } else if (e.Button == MouseButtons.Right) { cb.Diameter -= 1; } }; } }
如果你把这以上两段代码整理放入到一个winform程序中,看起来似乎效果已经达到了,但是,实际上这里面是有问题的,你可以猜猜trackBar.ValueChanged事件触发了几次,以及cb.DiameterChanged事件触发了几次。
我第一次分析代码时是这样的:以鼠标左键点击驱动事件为例,当鼠标左键点击时,cb.Diameter属性的set访问器被触发,假设此时传给set的value为101,在set访问器中我们没有做任何限制,所以会直接发布DiameterChanged(diameter)事件,订阅了该事件的方法会被执行,然后我们可以在Form1类中可以看到,由于我们写的订阅方法是匿名方法,所以直接执行方法体里的代码即trackBar.Value = value,这行代码会再发布trackBar的ValueChanged事件,继而执行订阅方法cb.Diameter = trackBar.Value,这又会触发一次DiameterChanged事件,也就是说,开启了新的一轮的事件触发循环。
可是,事实真是如此吗,我们在Form1中定义两个私有变量,分别用来统计两个订阅方法分别执行了几次,并修改订阅的匿名方法。代码如下:
cb.DiameterChanged += (value) => { trackBar.Value = value; System.Diagnostics.Debug.WriteLine($"DiameterChanged第{++cnt1}次执行"); }; trackBar.ValueChanged += (sender, e) => { cb.Diameter = trackBar.Value; System.Diagnostics.Debug.WriteLine($"ValueChanged第{++cnt2}次执行"); };
在输出中,我们可以看到结果:
DiameterChanged第1次执行
ValueChanged第1次执行
DiameterChanged第2次执行
这是为什么呢?难道不应该是无限循环输出吗?我一开始看到这个结果时也是疑惑的,后来,我猜想是因为trackBar的Value属性set访问器里,先判定了传进来的参数value是否跟当前Value相等,如果相等则不发布事件,事实上,我们能看到C#反编译后的代码,里面也确实是这么做的。源代码(.NET framework 4.7.2)如下:
public int Value { get { GetTrackBarValue(); return value; } set { if (this.value != value) { if (!initializing && (value < minimum || value > maximum)) { throw new ArgumentOutOfRangeException("Value", SR.GetString("InvalidBoundArgument", "Value", value.ToString(CultureInfo.CurrentCulture), "'Minimum'", "'Maximum'")); } this.value = value; SetTrackBarPosition(); OnValueChanged(EventArgs.Empty); } } }
当第二次发布DiameterChanged事件时,其订阅方法里,给trackbar赋值value时,此时赋的value和当前trackbar.Value相等,所以事件传播到此结束。
其实本来这篇博客我是想用两个自定义控件来做演示的,但再写一个自定义控件我怕大家没耐心看,所以干脆就拿trackbar做演示。不过看到这儿大家应该知道我想说明双向绑定时要注意的地方了,如果这个trackbar其Value属性的set访问器内部没有做判断限制的话,事件触发就会不停循环下去,为了避免这个问题,在必须在set访问器中做传入value和当前value的等值判断,如果不相等才去发布事件。两个控件双向数据绑定时,需要它们各自的绑定属性的set访问器中做等值判断,这样两个事件分别只发布一次。
这也是为什么我把CircleButton改了下的原因,现在我们把CircleButton的Diameter属性set访问器加上等值判断,然后看下两个事件是否分别只触发一次。
public partial class CircleButton : Button { public delegate void OnDiameterChanged(int value); public event OnDiameterChanged DiameterChanged; private int diameter = 100; public int Diameter { get { return diameter; } set { if (diameter != value) { diameter = value; SetDiameter(); DiameterChanged(diameter); } } } //... }
输出结果:
ValueChanged第1次执行
DiameterChanged第1次执行
浙公网安备 33010602011771号