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),點擊它會產生什么效果呢?
(圖 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只在于垃圾回收之后才會把委托事件給自動刪除。
最后弱弱的說一句如果你能夠在銷毀對象的時候加上-=類似的動作,應該就不需要勞累的寫這么多了。
轉載于:https://www.cnblogs.com/Curry/archive/2008/10/30/1322647.html
總結
以上是生活随笔為你收集整理的WPF学习笔记(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springMVC_数据的处理过程
- 下一篇: asp.net ajax控件工具集 Au