WPF学习笔记(三)
1.1 事件概括
第一节中我们给窗体添加了一个按钮,不过好像Button点个几下也只有些发光样式的变化,什么你还把系统皮肤去掉了?算了承认下确实够寒碜,那让我们再动动手。
1.1.1 路由事件简述
public HelloWorld()
{
Button button = new Button();
button.AddHandler(Button.ClickEvent,new RoutedEventHandler(button_Click));
……
}
void button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hello World");
}
本例中也可以由+=来操作如button.Click += new RoutedEventHandler(button_Click)
这段功能好像没有什么出奇的,就是给button添加了个Click事件,效果无非是跳出个Hello World的提示框.可大家有没注意到它的委托字样变了RoutedEventHandler,MS中文定义为路由事件,那为什么叫作路由呢,要知道路由器我都坏了好几个,完全勾起了那段伤痛的回忆(以下省略悲伤3万字)。路由(Router)我们知道是安排线路的人,那么Route是什么意思呢?有人可能已经按捺不住:就是安排线路的意思嘛。完全正确一百分,可惜没有任何奖励^-^。
1.1.2 附加事件
在WPF中大部分类派生自UIElement 或 ContentElement,如前Button和Window便派生于UIElement ,这些类可以是任一路由事件的事件侦听器, 这个和附加属性有些相似,你本身可以没有此事件,但要附加的事件必须是先存在的。在上例构造函数中增加这么一句:
this.AddHandler(Button.ClickEvent, new RoutedEventHandler(window_Click));
就是window为它容器内类型为Button或是继承自Button的元素增加了一个点击事件(ClickEvent),而本来Button的点击事件是只有Button或是派生类才有的,增加该句后window也可以捕获button的click事件;要问为什么this就是算作窗体,因为我们这个类继承自window,如果继承自Button那this就代表按钮了,而且此事件会对他本身也有效了.
1.1.3 冒泡
如果父容器也可以引发子元素的事件,那么有个相同的事件一起引发会是怎么样的结果呢,我们不要忘记程序员优良而光荣的传统,敲几行代码先。
public HelloWorld()
{
this.AddHandler(Button.ClickEvent, new RoutedEventHandler(window_Click));
StackPanel parentPanel = new StackPanel();
parentPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(parentPanel_Click));
Button button = new Button();
button.AddHandler(Button.ClickEvent, new RoutedEventHandler(button_Click));
button.Name = "firstButton";
button.Content = "I'm a button";
parentPanel.Children.Add(button);
this.Content = parentPanel;
}
void window_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("window");
}
void parentPanel_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("parentPanel");
}
void button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("button");
}
上面这段代码定义了一个StackPanel容器parentPanel,并把我们刚才的button按钮放到里面,通过 parentPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(parentPanel_Click));
为button又订阅了一个事件,让button在点击时弹出一个写有”parentPanel”字样的对话框;Window也订阅了一个相同的事件让其弹出”window”字样的对话框,最后一句this.Content = parentPanel;是指窗体内容由StackPanel来填充。
三个元素(Window, StackPanel,Button)都分别的对Button这个按钮的点击事件(Click)作了定义,而窗体中只有名为button 的一个按钮见(图 2-1),点击它会产生什么效果呢?
别瞎想了,运行点击下不就得了,结果是跳出了MessageBox,那不是废话嘛!关键不是一个是三个!
三个弹出框依次是”button”->” parentPanel”->”window”。这样我们可以得出结论系统对Click这个事件的加载顺序是先从我们所见源元素再一层层向上引发的,我们称这叫做冒泡(Bubble)。
1.1.4 隧道
既然有从所见层到最外程的概念,那有没有从外层到内层的概念呢,也就是说”window”->” parentPanel”->”button” 这样的弹出顺序,世上阴阳对存无独有偶,明显是有的,它叫做 隧道(Tunnel),也可以理解为从外层开了条隧道到内部;这点在做复合控件的时候比较有用,你应该希望得到消息最先是控件本身而不是内部的子控件.隧道又称预览,比如WPF的默认输入事件(mouseUp,mouseDown,keyUp),是通过隧道和冒泡结合来绑定同一个参数,KeyDown 事件和 PreviewKeyDown 事件具有相同的签名,前者是冒泡输入事件,后者是隧道输入事件,隧道是比冒泡先引发的。(一般Preview带头的是隧道)
1. 针对根元素(window)处理 PreviewMouseDown(隧道)。
2. 针对中间元素(stackpanel)处理 PreviewMouseDown(隧道)。
3. 针对源元素(button)处理 PreviewMouseDown(隧道)。
4. 针对源元素(button)处理 MouseDown(冒泡)。
5. 针对中间元素(stackpanel)处理 MouseDown(冒泡)。
6. 针对根元素(window)处理 MouseDown(冒泡)。
这里要强调的是隧道优先然后冒泡指的仅仅是WPF里的默认的输入事件,如果自定义控件的话,先冒泡后隧道还是先隧道后冒泡都是控件开发者自己来决定的,其实说白了也就个参数共享和事件调用顺序而已。
1.1.5 直接和模拟类似直接
有的人可能不理解了,我就是要点下按钮,就是让那个”button”弹出就可以了,其他的关我啥事,也就是说源元素本身才响应事件,这也是winform中默认定义的事件,WPF中称为直接事件(Direct),当然在本例中系统已经定义Click的事件为冒泡(Bubble)那该怎么做呢,难道就束手无策了,让对话框只能接二连三的弹出?MS当然不可能没有考虑到这点,你只要在button事件中添加e.Handled = true;这句如 :
void button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("button");
e.Handled = true;
}
这样的话” parentPanel”,”window”对话框就弹不出来了,话音未落,只见又有人提出了更为苛刻的需求:我只要求button和window弹出对话框,该怎么办?
当然可以,事实上事件的原理就是把函数指针放入到一个队列中,出队无论如何都是要轮一遍加入到队列中的事件,e.Handled = true;并没有把事件队列给停止,打个比方,你去排队买票,已经有2个人在你前面(你们去的是同一地),无论他们是否买的到票(这年月买票也不容易),除有人中途离开(被RemoveHanlded),正常情况下你都要等两人去窗台询问是否有票可以买后才能被轮到,询问过程就是e.Handled,如果被定为true说明票已经卖完,那么按照逻辑剩下的人应该是无法买到票的,可假定我事前就有预定,不管前面的人是否买到票也能拿票回家。
那怎么预定票呢?在程序中我们只需要把window的事件在增加的时候就定义为必须触发就可以了this.AddHandler(Button.ClickEvent, new RoutedEventHandler(window_Click),true);第三个参数handleEventsToo设置为true即可.如果设为false在e.Handled = true;的时候就不引发.
handleEventsToo方法适用于冒泡(Bubble)和隧道(Tunnel),局限性在于不能在 XAML 中像使用属性那么定义,只能通过后端程序(CLR)来注册。
1.1.6 如何自定义事件
说了这么多,想必你也想自己定义个事件试试,下面我们借用MSDN的例子自定义一个继承自Button的控件MyButtonSimple.
public class MyButtonSimple : Button {
//创建自定义路由事件的第一步便是用EventManager先注册一个,此例用的是冒泡事件
public static readonly RoutedEvent TapEvent = EventManager.RegisterRoutedEvent(
"Tap", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyButtonSimple));
//Clr的属性声明,对于XAML该属性只是为了编译通过
public event RoutedEventHandler Tap
{
add { AddHandler(TapEvent, value); }
remove { RemoveHandler(TapEvent, value); }
}
//引发事件
void RaiseTapEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(MyButtonSimple.TapEvent);
RaiseEvent(newEventArgs);
}
//重载onclick事件的具体实现, 当点击按钮的时候便能引发我们自定义的Tap事件
protected override void OnClick()
{
RaiseTapEvent();
}
}
其中引发事件我们之前的做法可能是
if (Tap != null)
Tap(this, new RoutedEventArgs());
但如今因为我们定义的事件是放在一个Dictionary容器中外部无法操控,所以只能运用系统的RaiseEvent函数进行引发事件操作,不过最让人心烦的是RaiseEvent中的参数只接受RoutedEventArgs类型,这就意味着,我们不能自定义多个参数的委托,多参数必须是派生自RoutedEventArgs类后通过增加属性来实现,例如Button的Drag事件, public delegate void DragEventHandler(object sender, DragEventArgs e);所以只能老老实实的和MS的做法一样,定义一个为object类型的事件发出者和一个继承自RoutedEventArgs的参数e。注:DragEventArgs派生于RoutedEventArgs。
本节的示例程序有window,stackpanel,button多个元素引发事件,怎样才能找出源元素?
void HandleClick(object sender, RoutedEventArgs e)
{
// 这里的sender如果是window引发的则是window,当然Button引发的便是Button
Button srcButton = e.Source as Button;//原始引发者
srcButton.Width = 200;
}
1.1.7 为路由事件添加类处理
在冒泡示例中只定义了一个stackpanel,假如我们现在走极端,需要声明一百个stackpanel类,每个类都需要为子元素的Button弹出一个对话框,难道只能每次在stackpanel声明后增加一行对应的语句来实现?有没有种更快捷的方法,如对stackpanel类型一次性注册点击事件。在WPF中可以用EventManager.RegisterClassHandler来注册,我们只需要在静态构造函数中这样定义:
static HelloWorld()
{
EventManager.RegisterClassHandler(typeof(Button), Button.ClickEvent, new RoutedEventHandler(button_Click));
}
这样定义的一个缺点 是,一旦你注册了,你使用到的此类型都会注册这个事件,而问题还在于无法把此注册删除,除非一个个删除,至少我没发现捷径-_-!。
如果基类和子类均注册了类处理功能,则将首先调用子类的处理程序。
1.1.8 弱事件 WeakEvent
在讲此之前让我们先来看一个这样的例子:
public delegate void MessageWriter(string text);
public class EventClass
{
public event MessageWriter DoEvent;
public void ConsoleOut(string text)
{
if (DoEvent!=null)
DoEvent(text);
}
}
public class ConsoleWriter
{
public void WriteToConsole(string text)
{
Console.Write(text);
}
}
// Client code...
ConsoleWriter writerObj = new ConsoleWriter();
EventClass evetObj = new EventClass();
evetObj.DoEvent += new MessageWriter(writerObj.WriteToConsole);
//writerObj = null;
//GC.Collect();
evetObj.ConsoleOut("Hello, World!");
这段代码应该没有什么难理解的地方,只是演示了一个类(EventClass)中的事件委托MessageWriter由另一个类(ConsoleWriter)的WriteToConsole函数来实现.关键在于被注释掉的这句
writerObj = null;
GC.Collect();
把注释去掉的话,我们希望程序是运行错误的,因为对象已经被删除了,事件中的委托理应跟随着对象的删除而消失,可问题是去掉注释后,我们仍然可以成功的运行程序并输出Hello,World.这是为什么呢?其实委托的绑定是个强类型引用,被引用到的东西是不会被垃圾回收器销毁的,除非我们用
evetObj.DoEvent -= new MessageWriter(writerObj.WriteToConsole);
显示删除,这样带来的一个隐患是,如若我们忘记删除那么就会造成内存泄漏。
怎样才能够在对象被销毁时跟随的事件也一起被废除呢?
MS在WPF中引入了WeakEvent模式,MSDN讲解了实现 WeakEvent 模式由三个方面组成:
- 从 WeakEventManager 类派生一个管理器。
- 在任何想要注册弱事件的侦听器的类上实现 IWeakEventListener 接口,而不生成源的强引用。
- 注册侦听器时,对于想要侦听器使用该模式的事件,不要使用该事件的常规的 add 和 remove 访问器,请在该事件的专用 WeakEventManager 中改用“AddListener”和“RemoveListener”实现。
也就是每个要用到的事件要有一个从WeakEventManager 类派生的管理器,而引用到的类要继承于IWeakEventListener接口并实现里面的方法,注册侦听器时不要用之前的add,remove或+=,-=来访问,要用对应该事件的WeakEventManager管理器中的AddListener和RemoveListener方法实现.
下面让我们先来改写委托并实现从WeakEventManager派生的TextEventManager
public class TextEventArgs : EventArgs
{
public TextEventArgs(string text)
{
Text = text;
}
public string Text { get; set; }
}
public delegate void MessageWriter(object sender, TextEventArgs e);
public class TextEventManager : WeakEventManager
{
private TextEventManager()
{
}
public static void AddListener(EventClass source, IWeakEventListener listener)
{
//调用WeakEventManager的保护方法来注册
CurrentManager.ProtectedAddListener(source, listener);
}
private void OnTextPrint(object sender, TextEventArgs e)
{
//这句就是调用类的IWeakEventListener接口,如果类无法处理返回false会发出异常
base.DeliverEvent(sender, e);
}
public static void RemoveListener(EventClass source, IWeakEventListener listener)
{
//调用WeakEventManager的保护方法来注销
CurrentManager.ProtectedRemoveListener(source, listener);
}
protected override void StartListening(object source)
{
EventClass changed = (EventClass)source;
changed.DoEvent += new MessageWriter(this.OnTextPrint);
}
protected override void StopListening(object source)
{
EventClass changed = (EventClass)source;
changed.DoEvent -= new MessageWriter(this.OnTextPrint);
}
private static TextEventManager CurrentManager
{
get
{
Type managerType = typeof(TextEventManager);
//查看 TextEventManager 是否已被注册,没有注册将返回null
TextEventManager currentManager = (TextEventManager)WeakEventManager. GetCurrentManager(managerType);
if (currentManager == null)
{
currentManager = new TextEventManager();
//第一次调用的话把该Manager进行注册
WeakEventManager.SetCurrentManager(managerType, currentManager);
}
return currentManager;
}
}
}
从其中的StartListening和StopListening方法中可以看出其实是当发现调用类被销毁时来显示把委托给删除的。
不知道你有没感觉到为了一个事件而要写这么个Manager似乎有些烦,,幸好EventClass和ConsoleWriter变化倒不是很大。
注意:ReceiveWeakEvent实现了调用则返回 true,接收到的事件不是预期的事件则返回 false.
public class EventClass
{
public event MessageWriter DoEvent;
public void ConsoleOut(string text)
{
if (DoEvent != null)
DoEvent(this,new TextEventArgs(text));
}
}
public class ConsoleWriter : IWeakEventListener
{
private void WriteToConsole(string text)
{
Console.Write(text);
}
public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
if (managerType == typeof(TextEventManager))
{
WriteToConsole(((TextEventArgs)e).Text);
}
else
{
return false;
}
return true;
}
}
// Client code...
ConsoleWriter writerObj = new ConsoleWriter();
EventClass evetObj = new EventClass();
TextEventManager.AddListener(evetObj, writerObj);
//TextEventManager.RemoveListener(evetObj, writerObj);
writerObj = null;
//GC.Collect();
evetObj.ConsoleOut("Hello, World!");
当然你没有把GC.Collect()的注释去掉的话依旧有Hello,World被打印,所以WeakEvent只在于垃圾回收之后才会把委托事件给自动删除。
最后弱弱的说一句如果你能够在销毁对象的时候加上-=类似的动作,应该就不需要劳累的写这么多了。