WPF學習筆記(三)

來源:互聯網
上載者:User
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只在於記憶體回收之後才會把委託事件給自動刪除

最後弱弱的說一句如果你能夠在銷毀對象的時候加上-=類似的動作,應該就不需要勞累的寫這麼多了。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.