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),点击它会产生什么效果呢?

(图 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管理器中的AddListenerRemoveListener方法实现.

下面让我们先来改写委托并实现从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只在于垃圾回收之后才会把委托事件给自动删除

最后弱弱的说一句如果你能够在销毁对象的时候加上-=类似的动作,应该就不需要劳累的写这么多了。

转载于:https://www.cnblogs.com/Curry/archive/2008/10/30/1322647.html

WPF学习笔记(三)相关推荐

  1. J2EE学习笔记三:EJB基础概念和知识 收藏

    J2EE学习笔记三:EJB基础概念和知识 收藏 EJB正是J2EE的旗舰技术,因此俺直接跳到这一章来了,前面的几章都是讲Servlet和JSP以及JDBC的,俺都懂一些.那么EJB和通常我们所说的Ja ...

  2. tensorflow学习笔记(三十二):conv2d_transpose (解卷积)

    tensorflow学习笔记(三十二):conv2d_transpose ("解卷积") deconv解卷积,实际是叫做conv_transpose, conv_transpose ...

  3. Ethernet/IP 学习笔记三

    Ethernet/IP 学习笔记三 原文为硕士论文: 工业以太网Ethernet/IP扫描器的研发 知网网址: http://kns.cnki.net/KCMS/detail/detail.aspx? ...

  4. iView学习笔记(三):表格搜索,过滤及隐藏列操作

    iView学习笔记(三):表格搜索,过滤及隐藏某列操作 1.后端准备工作 环境说明 python版本:3.6.6 Django版本:1.11.8 数据库:MariaDB 5.5.60 新建Django ...

  5. 吴恩达《机器学习》学习笔记三——多变量线性回归

    吴恩达<机器学习>学习笔记三--多变量线性回归 一. 多元线性回归问题介绍 1.一些定义 2.假设函数 二. 多元梯度下降法 1. 梯度下降法实用技巧:特征缩放 2. 梯度下降法的学习率 ...

  6. Python基础学习笔记三

    Python基础学习笔记三 print和import print可以用,分割变量来输出 import copy import copy as co from copy import deepcopy ...

  7. Mr.J-- jQuery学习笔记(三十二)--jQuery属性操作源码封装

    扫码看专栏 jQuery的优点 jquery是JavaScript库,能够极大地简化JavaScript编程,能够更方便的处理DOM操作和进行Ajax交互 1.轻量级 JQuery非常轻巧 2.强大的 ...

  8. WPF学习笔记(数据绑定篇3)

    接上回的<WPF学习笔记(数据绑定篇2)>,继续 BindValidation 此示例演示了: 如何使用错误模板: 使用样式显示错误信息: 如何在校验发生异常时执行回调: 首先,你可以看见 ...

  9. WPF学习笔记(7):DataGrid中数字自定义格式显示

    WPF学习笔记(7):DataGrid中数字自定义格式显示 原文:WPF学习笔记(7):DataGrid中数字自定义格式显示 DataGrid中数据显示如下图,数据格式比较杂乱.希望达到以下要求:(1 ...

最新文章

  1. AI一分钟|AI聊机器人“Shibuyu Mirai”获日本居住权,传今日头条3亿美元收购Faceu激萌
  2. R 语言柱状图示例笔记
  3. 三十五、虚拟内存的基本概念
  4. CSP认证 201503-3 节日[C++题解]:模拟、枚举、日期题、日期模板题
  5. PPC 调用约定 r0-r31寄存器介绍
  6. mvc @html.textboxfor 添加正则表示式,如何在C#/ MVC 4中的Html.TextBoxFor中输入占位符文本...
  7. 埋点套路深,千万别掉“坑”
  8. totolink 异地组网
  9. jsr303 自定义消息_JSR 303从I18N属性文件加载消息
  10. XVI Open Cup named after E.V. Pankratiev. GP of Eurasia
  11. 教你七招提高.NET网站性能
  12. 《代码敲不队》第九次团队作业:Beta冲刺第1天
  13. 字节跳动确认入局车联网:满足车载场景的用户体验
  14. [Django学习] Django基础(5)_优化分页器
  15. linux something
  16. 【预测模型】基于matlab GUI BP神经网络+最小二乘法预测模型【含Matlab源码 208期】
  17. java数组元素的默认值_数组元素默认的初始值都是什么?
  18. Ubuntu18.04 安装 网易云音乐 解决 打不开的问题
  19. Qt Charts入门
  20. CSP-M2 B - HRZ 学英语

热门文章

  1. 前端学习(2912):MvvM的实现原理
  2. [html] 说说你对html中的置换元素和非置换元素的理解
  3. [css] 说说你对line-height是如何理解的?
  4. 工作261:ele-layont布局使用
  5. 前端学习(2537):vue源码解析2伪数组转换为真数组
  6. 前端学习(2379):调整初始目录结构
  7. 前端学习(662):逻辑运算符练习
  8. java面试题3 牛客:下面有关jdbc statement的说法错误的是
  9. 玩转oracle 11g(25):手工删除oracle归档日志后操作步骤 和修改用户默认解锁时间
  10. 计算机控制面板图标怎么删除,电脑如何找回消失的“添加或删除程序”图标