看完前一篇文章,应该对委托有了大致的了解,现在我来说说事件。在接触事件之前,先给大家说说委托链的概念。
其实委托链也是相当的简单,在现实生活中,小明叫小张买完车票之后,可能接着又让他带张电影票。在程序猿的世界里,应该去怎么实现呢?
//小张类 class xiaozhang { public static void BuyTicket() { Console.WriteLine("NND,每次都让我去买票,鸡人呀!"); } public static void BuyMovieTicket() { Console.WriteLine("我去,自己泡妞,还要让我带电影票!"); } } //小明类 class MrMing { //声明一个委托,其实就是个“命令” public delegate void BugTicketEventHandler(); } class Program { static void Main(string[] args) { //这里就是具体阐述这个命令是干什么的,本例是MrZhang.BuyTicket“小张买车票” MrMing.BugTicketEventHandler myDelegate = new MrMing.BugTicketEventHandler(xiaozhang.BuyTicket); myDelegate += xiaozhang.BuyMovieTicket; //这时候委托被附上了具体的方法 myDelegate(); Console.ReadKey(); } }
其实,我们只是在程序中加了 myDelegate += MrZhang.BuyMovieTicket;这时这个委托就相当于要做2件事情,先是买车票,再是买电影票拉!好了,关于委托就介绍到这里了,是不是并没有大家想的这么可怕呢,在下一回我们就接着大白话的EVENT的讲解。
那什么是事件?EVENT?点击事件?加载事件?一连串的模糊的概念冲击着我们弱小的脑袋。
那我们首先来看一下比较正统的感念吧:
事件:是类在发生其关注的事情时用来提供通知的一种方式。
事件的发生一般都牵扯2个角色
事件发行者(Publisher):一个事件的发行者,也称作是发送者(sender),其实就是个对象,这个对象会自行维护本身的状态信息,当本身状态信息变动时,便触发一个事件,并通知所有的事件订阅者。
事件订阅者(Subscriber):对事件感兴趣的对象,也称为Receiver,可以注册感兴趣的事件,在事件发行者触发一个事件后,会自动执行这段代码。
为了更好的让大家理解上面的概念,我先什么都不讲,我们先来看一段简单的代码:
//发布者(Publiser) public class Publisher { //声明一个出版的委托 public delegate void PublishEventHander(); //在委托的机制下我们建立以个出版事件 public event PublishEventHander OnPublish; //事件必须要在方法里去触发,出版社发布新书方法 public void issue() { //如果有人注册了这个事件,也就是这个事件不是空 if (OnPublish != null) { Console.WriteLine("最新一期的《火影忍者》今天出版哦!"); OnPublish(); } } } //Subscriber 订阅者,无赖小明 public class MrMing { //对事件感兴趣的事情,这里指对出版社的书感兴趣 public static void Receive() { Console.WriteLine("嘎嘎,我已经收到最新一期的《火影忍者》啦!!"); } } //Subscriber 订阅者,悲情人物小张 public class MrZhang { //对事件感兴趣的事情 public static void Receive() { Console.WriteLine("幼稚,这么大了,还看《火影忍者》,SB小明!"); } } class Story { public static void Main(string[] args) { //实例化一个出版社 Publisher publisher = new Publisher(); //给这个出火影忍者的事件注册感兴趣的订阅者,此例中是小明 publisher.OnPublish += new Publisher.PublishEventHander(MrMing.Receive); //另一种事件注册方式 //publisher.OnPublish += MrMing.Receive; //发布者在这里触发出版火影忍者的事件 publisher.issue(); Console.ReadKey(); } }
如果童靴们,从上到下仔细看一边的话,我想应该知道什么是发布者,什么是订阅者了吧,那至于事件呢?
我们先看这句
publisher.OnPublish += new Publisher.PublishEventHander(MrMing.Receive);
这就是小明向出版社订阅他喜欢看的火影忍者,小张没有订阅所以小张没有收到书,我们再仔细看看这个赋值语句,是不是似曾相识过呢?是的就是我们在上一讲,在讲委托声明的时候,简直就是一个眸子里刻出来的嘛。
委托赋值:
BugTicketEventHandler myDelegate = new BugTicketEventHandler(MrZhang.BuyTicket);
所以,大家不要对事件有什么好怕的,其实事件的本质就是一个委托链,我们看一下事件的声明:
//声明一个出版的委托 public delegate void PublishEventHander(); //在委托的机制下我们建立以个出版事件 public event PublishEventHander OnPublish;
在我们使用事件的时候,必须要声明对应的委托,而触发事件,其实就是在使用委托链。好了大家先消化消化这讲的内容,我们在下一讲里,我们会讲到我们期盼已久的Sender,e两个神秘的参数了。为什么我们在日常的编程活动中遇到这么多sender,EventArgs e 参数呢?
protected void Page_Load(object sender, EventArgs e) { } protected void btnSearch_Click(object sender, ImageClickEventArgs e) { } protected void grdBill_RowDataBound(object sender, GridViewRowEventArgs e) { }
那他们到底表示什么呢?
在回答上面的问题之前,我们先搞懂 .Net Framework的编码规范:
一、委托类型的名称都应该以EventHandler结束。
二、委托的原型定义:有一个void返回值,并接受两个输入参数:一个Object 类型,一个 EventArgs类型(或继承自EventArgs)。
三、事件的命名为 委托去掉 EventHandler之后剩余的部分。
四、继承自EventArgs的类型应该以EventArgs结尾。
这就是微软编码的规范,当然这不仅仅是规则,而是在这种规则下使程序有更大的灵活性,那我们就继续重构之前的例子,让他符合微软的规范。
//所有订阅者【Subscriber】感兴趣的对象,也就是e,都要继承微软的EventArgs //本例中订阅者【也称观察者】MrMing,MrZhang他们感兴趣的e对象,就是杂志【magazine】 public class PubEventArgs : EventArgs { public readonly string magazineName; public PubEventArgs() { } public PubEventArgs (string magazineName) { this.magazineName = magazineName; } } //发布者(Publiser) public class Publisher { //声明一个出版的委托 //这里多了一个参数sender,它所代表的就是Subject,也就是监视对象,本例中就是Publisher public delegate void PublishEventHander(object sender ,PubEventArgs e); //在委托的机制下我们建立以个出版事件 public event PublishEventHander Publish; //声明一个可重写的OnPublish的保护函数 protected virtual void OnPublish(PubEventArgs e) { if (Publish != null) { //Sender = this,也就是Publisher this.Publish(this, e); } } //事件必须要在方法里去触发 public void issue(string magazineName) { OnPublish(new PubEventArgs(magazineName)); } } //Subscriber 订阅者 public class MrMing { //对事件感兴趣的事情 public static void Receive(object sender,PubEventArgs e) { Console.WriteLine("嘎嘎,我已经收到最新一期的《"+e.magazineName+"》啦!!"); } } public class MrZhang { //对事件感兴趣的事情 public static void Receive(object sender, PubEventArgs e) { Console.WriteLine("幼稚,这么大了,还看《火影忍者》,SB小明!"); Console.WriteLine("这个我定的《"+e.magazineName+"》,哇哈哈!"); } } class Story { public static void Main(string[] args) { //实例化一个出版社 Publisher publisher = new Publisher(); Console.Write("请输入要发行的杂志:"); string name = Console.ReadLine(); if (name == "火影忍者") { //给这个出火影忍者的事件注册感兴趣的订阅者,此例中是小明 publisher.Publish += new Publisher.PublishEventHander(MrMing.Receive); //发布者在这里触发出版火影忍者的事件 publisher.issue("火影忍者"); } else { //给这个出火影忍者的事件注册感兴趣的订阅者,此例中是小明[另一种事件注册方式] publisher.Publish += MrZhang.Receive; publisher.issue("环球日报"); } Console.ReadKey(); } }
输入“火影忍者”后,触发小明订阅的事件
显示
通过例子我再做一次说明,其实我们不用把Sender,e想的过于可怕,
一、委托声明原型中的Object类型的参数代表了Subject,也就是监视对象,在本例中是 Publisher(出版社)
二、EventArgs 对象包含了Observer所感兴趣的数据,在本例中是杂志。
大家先休息休息,让大脑放松放松,先让我们看句名人名言:
很多事情就像看A片,看的人觉得很爽,做的人未必!
“中国人的励志和国外的励志存在非常大的不同,中国的励志比较鼓励人立下大志愿,卧薪尝胆,有朝一日成富成贵。而国外的励志比较鼓励人勇敢面对现实生活,面对普通人的困境,虽然结果也是成富成贵,但起点不一样,相对来说,我觉得后者在操作上更现实,而前者则需要用999个失败者来堆砌一个成功者的故事。”
好了,我们接着讲我们的委托与事件,其实如果大家对设计模式精通的话,其实他们关联的是观察者(Observer)模式,这里我就不再描述什么是观察者模式了,只是简单讲一下他们的关联:
在C#的event中,委托充当了抽象的Observer接口,而提供事件的对象充当了目标对象。委托是比抽象Observer接口更为松耦合的设计。
如果看不懂的话也没关系,当大家OO达到一定程度了,自然而然就会明白。最后我们来看一个我们日常最最常用的观察者模式:
场景:当我们用信用卡刷完钱的时候,我们就会接收到手机短信,或者是电子邮件,其实这就是Observer pattern。
//---本例场景为当用户从银行账号里取出钱后,马上通知电子邮件和发手机短信--- //本例中的订阅者,也就是观察者是电子邮件与手机 //发布者,也就是被监视对象是银行账号 //Obverser电子邮件,手机关心的对象e ,分别是邮件地址、手机号码、取款金额 public class UserEventArgs : EventArgs { public readonly string emailAddress; public readonly string mobilePhone; public readonly string amount; public UserEventArgs(string emailAddress, string mobilePhone,string amount) { this.emailAddress = emailAddress; this.mobilePhone = mobilePhone; this.amount = amount; } } //发布者,也就是被监视的对象-银行账号 class BankAccount { //声明一个处理银行交易的委托 public delegate void ProcessTranEventHandler(object sender, UserEventArgs e); //声明一个事件 public event ProcessTranEventHandler ProcessTran; protected virtual void OnProcessTran(UserEventArgs e) { if (ProcessTran != null) { ProcessTran(this, e); } } public void Prcess(UserEventArgs e) { OnProcessTran(e); } } //观察者Email class Email { public static void SendEmail(object sender, UserEventArgs e) { Console.WriteLine("向用户邮箱" + e.emailAddress + "发送邮件:您在"+System.DateTime.Now.ToString()+"取款金额为"+e.amount); } } //观察者手机 class Mobile { public static void SendNotification(object sender, UserEventArgs e) { Console.WriteLine("向用户手机" + e.mobilePhone + "发送短信:您在" + System.DateTime.Now.ToString() + "取款金额为" + e.amount); } } //订阅系统,实现银行系统订阅几个Observer,实现与客户端的松耦合 class SubscribSystem { public SubscribSystem() { } public SubscribSystem(BankAccount bankAccount, UserEventArgs e) { //现在我们在银行账户订阅2个,分别是电子邮件和手机短信 bankAccount.ProcessTran += new BankAccount.ProcessTranEventHandler(Email.SendEmail); bankAccount.ProcessTran += new BankAccount.ProcessTranEventHandler(Mobile.SendNotification); bankAccount.Prcess(e); } } class Client { public static void Main(string[] args) { Console.Write("请输入您要取款的金额:"); string amount = Console.ReadLine(); Console.WriteLine("交易成功,请取磁卡。"); //初始化e UserEventArgs user = new UserEventArgs("[email protected]", "18868789776",amount); //初始化订阅系统 SubscribSystem subject = new SubscribSystem(new BankAccount(), user); Console.ReadKey(); } }
网上还有个热水器烧水的OBSERVER PATTERN 也是蛮经典的,大家可以看看。
接着,我们要讲讲,在我们的日常的MES系统开发中,到底在什么场景用到委托事件,毕竟我们学了这样的技术,我们必须要用起来,这才有价值嘛!
一、通用控件场景
通用控件有很多,这里举最常用的万能通用分页控件
【注:】本分页控件,只是为了讲解使用,并非真分页控件,还是基于.net控件的分页
我们先来看场景
我们所看到的这个分页控件就是这次讲解的主角,在日常的编程中,像这样功能我们用的做多,所以我们必须要把它抽象出来,不能每个页面都写分页逻辑吧。那我们想想到底怎样去实现这样的功能呢?怎样才能让页面知道我们按了控件的哪个按钮呢?这时候让我们联想一想委托与事件,一定要聚精会神,叮咚!有了,我们可以把这个控件想象成一个发布者,而各个页面就想象成订阅者,或者是观察者,当页面订阅了分页控件的事件后,自然就会相应了嘛?有了思路,我们就开始行动吧!
第一步,我们编写发布者代码,也就是这个控件代码:
//和我们上一讲讲的一样,我们先定义订阅者所感兴趣的对象,这里我们将它抽象成Action,也就是首页、下页、上页之类的动作 public class PageChangeEventArgs : EventArgs { private string action = string.Empty; public string Action { get { return this.action; } set { this.action = value; } } public PageChangeEventArgs() { } public PageChangeEventArgs(String paramAction) { this.Action = paramAction; } } //发布者代码 也就是控件代码 public partial class UIPageControlsNavigator : System.Web.UI.UserControl { //这里我们声明一个页面改变的委托[注:命名一定要规范] public delegate void PageChangeEventHandler(object sender, PageChangeEventArgs e); //这里我们声明一个事件 public event PageChangeEventHandler PageChange; //然后以个保护类型的OnPageChange方法 protected virtual void OnPageChange(PageChangeEventArgs e) { if (PageChange != null) { PageChange(this, e); } } //这里就是上一讲中,具体的触发函数,这里变成了一个按钮触发事件 protected void lbtnFirst_Click(object sender, EventArgs e) { //这时候,订阅者关心的对象e,也就是触发的是“首页”这个按钮 OnPageChange(new PageChangeEventArgs("First")); } protected void lbtnPrevious_Click(object sender, EventArgs e) { //这时候,订阅者关心的对象e,也就是触发的是“下页”这个按钮 OnPageChange(new PageChangeEventArgs("Previous")); } protected void lbtnLast_Click(object sender, EventArgs e) { OnPageChange(new PageChangeEventArgs("Last")); } protected void lbtnNext_Click(object sender, EventArgs e) { OnPageChange(new PageChangeEventArgs("Next")); } protected void btnSearch_Click(object sender, EventArgs e) { if (IsNumber(txtSearchPageCount.Text)) { OnPageChange(new PageChangeEventArgs("Search")); } } protected void cmbPerPage_SelectedIndexChanged(object sender, EventArgs e) { OnPageChange(new PageChangeEventArgs("PageSizeChanged")); } .... #region public void BindData(GridView myGridView, IList businessObjects, PageChangeEventArgs e) /// <summary> /// 具体控件分页功能实现 /// </summary> /// <param name="myGridView">当前Grid控件</param> /// <param name="businessObjects">数据源</param> /// <param name="e">事件对象</param> public void BindData(GridView myGridView, IList businessObjects, PageChangeEventArgs e) { // 计算页面数 if (businessObjects == null) { this.RowCount = 0; } else { this.RowCount = businessObjects.Count; } double pageCount = (double)RowCount / this.PageSize; this.PageCount = (int)Math.Ceiling(pageCount); myGridView.DataSource = businessObjects; myGridView.PageSize = this.PageSize; switch (e.Action) { case "PageLoad": if (CurrentPage > 0) { myGridView.PageIndex = CurrentPage - 1; } break; case "First": myGridView.PageIndex = 0; myGridView.EditIndex = -1; break; case "Previous": myGridView.PageIndex--; myGridView.EditIndex = -1; break; case "Next": myGridView.PageIndex++; myGridView.EditIndex = -1; break; case "Last": myGridView.PageIndex = this.PageCount - 1; myGridView.EditIndex = -1; break; case "PageSizeChanged": myGridView.PageIndex = 0; myGridView.EditIndex = -1; break; case "Search": myGridView.PageIndex = int.Parse(txtSearchPageCount.Text) - 1; break; case "Refresh": break; default: myGridView.PageIndex = 0; break; } // 页数不够了,进行调整 if (myGridView.PageIndex >= this.PageCount) { myGridView.PageIndex = this.PageCount == 0 ? 0 : this.PageCount - 1; } myGridView.DataBind(); // 获取按钮的状态 this.GetButtonState(myGridView); } #endregion }
当然控件代码还不值这些,我这里就列举出我们委托事件需要的代码:
然后我们看一下调用页面的代码,也就是观察者,本例中是角色页面RoleManage.aspx
//角色管理页面代码类 public partial class RoleManage : BasePage { protected void Page_Load(object sender, EventArgs e) { //在当前页面订阅控件的点击事件 this.myNavigator.PageChange += new PageChangeEventHandler(this.myNavigator_PageChange); if (!Page.IsPostBack) { } } //具体的点击触发函数功能,这里就是控件的分页 private void myNavigator_PageChange(object sender, PageChangeEventArgs e) { this.GetAllRoles(e); } private void GetAllRoles(PageChangeEventArgs e) { try { //角色数据源 roles = roleService.GetAllRoles(); //调用控件的分页功能函数,这个封装在分页控件里可以,封装在通用的类库里也行 this.myNavigator.BindData(this.grdRole, roles, e); } catch (Exception myException) { return; } } }
其实原理很简单,当控件上按下下页或者其它按钮的时候,这时候因为角色管理页面已经订阅了这个事件,所以它会执行具体委托的那个实体函数,就这么简单。大家了看了可能会头大,那就自己动手试着做一下,只有做了才能真正的体会到里面的奥妙,其实和我上一讲内容很相识,只是稍微有一点点的变化而已。
二、业务控件场景
大家在做MES系统的时候,50%的时候是在复制黏贴,甚至有的时候有些逻辑老是复制到这里,然后复制到那里,当然起初的时候感觉很快,也不用动脑子ctrl+c,ctrl+v结束,但是到后来逻辑改了,那时候就像没头苍蝇似的,早就忘了到底有多少地方用到这些逻辑,所以往往到BUG发生的时候,才恍然大悟“哦!原来这里忘了改了!”等等。我并不反对大家ctrl+c,ctrl+v,但是在享受这样的快捷之后,腾出时间再来重构一下,看看这时候能否用是否能抽象呀?用设计模式?符不符合00的设计原则?不然你就是编10年的软件,又能得到什么样的提高!又扯远了。。。
接下来,我们具体来看场景:
这是一个工作流审批用户控件,做MES系统的其实经常会和这个打交道,然而我们把这个逻辑封装成一个控件,那我们在今后维护上将会减轻很多工作量。这里我只介绍这控件技术上我们用到的委托和事件的代码:
public partial class ApprovalResults : System.Web.UI.UserControl { //EventHandler是微软默认的委托,在本例中我们直接就用EventHandler来表示委托,当然它的参数是Sender,e public event EventHandler Preview; public event EventHandler Submit; public event EventHandler FMOK; public event EventHandler FMProgress; public event EventHandler FMSave; ... //提交按钮事件 protected void btnSubmit_Click(object sender, EventArgs e) { try { //触发我们定义的事件 Submit(sender, e); } catch (Exception ex) { ... } } ... }
然后我们到订阅这个事件的页面上看一下代码
public partial class Preview : BasePage { protected void Page_Load(object sender, EventArgs e) { ... //在次页面中订阅审批控件的提交按钮事件 ApprovalResults1.Submit += new EventHandler(ApprovalResults1_Submit); } //具体的提交事件功能函数 public void ApprovalResults1_Submit(object sender, EventArgs e) { try { } catch (Exception ex) { } finally { } } }
这样我想大家都理解了,当审批控件点击提交按钮,其实访问的就是订阅者页面的功能函数。
其实委托事件应用的场景还有很多,它就是观察者模式的提炼,我非常希望大家能看了我的文章后能有点收获。
(文章转摘自:大白话系列之C#委托与事件讲解(序言))