1. 程式人生 > >[總結]使用WPF路由事件過程中遇到的一些小問題

[總結]使用WPF路由事件過程中遇到的一些小問題

寫在前面

本文一開始會給出一個使用WPF路由事件的例項,因為本文所有的表述都將基於該例項。而本文所給例項來自於《WPF自定義路由事件》一文,在《WPF自定義路由事件》一文中會對例項程式碼做詳細說明,所以,大家在閱讀本文例項程式碼期間若存在疑問,可以先去看看《WPF自定義路由事件》一文,看是否能從中獲得你想要的解答。

本文例項

1 新建DetailReportEventArgs類,該類派生自RoutedEventArgs類,RoutedEventArgs類包含與路由事件相關的狀態資訊和事件資料。DetailReportEventArgs類中定義了屬性EventTime與EventPublishr,EventTime屬性記錄時間的發生時間,EventPublishr屬性記錄事件的釋出者。下面是DetailReportEventArgs類的完整程式碼。

//************************************************************  
//  
// WPF路由事件示例程式碼  
//  
// Author:三五月兒  
//   
// Date:2014/08/31 
//  
// http://blog.csdn.net/yl2isoft  
//  
//************************************************************  
 
using System;
using System.Windows;
 
namespace WpfRoutedEventExp
{
    public class DetailReportEventArgs : RoutedEventArgs
    {
        public DetailReportEventArgs(RoutedEvent routedEvent, object source)
            :base(routedEvent,source){}
        public DateTime EventTime { get; set; }
        public string EventPublisher { get; set; }
    }
}

2 新建DetailReportButton類,該類派生自Button類,為該類新增路由事件DetailReportEvent。下面是DetailReportButton類的完整程式碼。

//************************************************************  
//  
// WPF路由事件示例程式碼  
//  
// Author:三五月兒  
//   
// Date:2014/08/31 
//  
// http://blog.csdn.net/yl2isoft  
//  
//************************************************************  
using System;
using System.Windows;
using System.Windows.Controls;
 
namespace WpfRoutedEventExp
{
    public class DetailReportButton : Button
    {
        public static readonly RoutedEvent DetailReportEvent = EventManager.RegisterRoutedEvent("DetailReport",RoutingStrategy.Bubble,typeof(EventHandler<DetailReportEventArgs>),typeof(DetailReportButton));
        public event RoutedEventHandler  DetailReport
        {
            add { this.AddHandler(DetailReportEvent, value); }
            remove { this.RemoveHandler(DetailReportEvent, value); }
        }
        protected override void OnClick()
        {
            base.OnClick();
            DetailReportEventArgs args = new DetailReportEventArgs(DetailReportEvent, this);
            args.EventPublisher = this.ToString();
            args.EventTime = DateTime.Now;
            this.RaiseEvent(args);
        }
    }
}

3 下面給出使用DetailReportButton 類的完整程式碼,包括畫面程式碼以及畫面後端程式碼兩部分。

程式畫面程式碼:

<Window x:Class="WpfRoutedEventExp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfRoutedEventExp"
        Title="MainWindow" Height="350" Width="525">
    <Grid x:Name="Grid_FirstLayer" >
        <Grid x:Name="Grid_SecondLayer" >
            <Grid x:Name="Grid_ThirdLayer" >
                <local:DetailReportButton x:Name="Button_Confirm" Width="100" Height="100" Content="Click Me" Margin="142,111,250,100" />
            </Grid>
        </Grid>
    </Grid>
</Window>

程式畫面的後端程式碼:

//************************************************************  
//  
// WPF路由事件示例程式碼  
//  
// Author:三五月兒  
//   
// Date:2014/08/31 
//  
// http://blog.csdn.net/yl2isoft  
//  
//************************************************************ 
using System.Windows;
 
namespace WpfRoutedEventExp
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();            
            this.Grid_ThirdLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));
            this.Grid_SecondLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));
            this.Grid_FirstLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));
            this.Button_Confirm.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));
        }
 
        private void Button_Clicked1(object sender, RoutedEventArgs e)
        {
            FrameworkElement ele = sender as FrameworkElement;
            DetailReportEventArgs dre = e as DetailReportEventArgs;
            MessageBox.Show(dre.EventPublisher + "-->" + ele.Name + ";" + dre.EventTime);
        }
    }
}
4 執行程式,得到下圖所示效果。

 

圖1 程式執行效果圖

結果顯示:單擊畫面按鈕,觸發路由事件,路由事件的訊息便從事件的觸發者開始向它的上級容器控制元件一層一層的往外傳,直到最外層的容器控制元件為止。

你我的約定

因為下面的說明中會反覆修改例項程式碼,現在給出約定:

每一次修改在說明完相關的知識點後都將回退到修改前的狀態。

我的新聞釋出會

下面將以問答的方式來總結我在使用WPF路由事件時遇到或想到的幾個小問題。

Q1 :除了使用AddHandler方法外,還有什麼方法可以將想監聽的事件與事件的處理器關聯起來?

可以在XAML程式碼中將監聽的事件與事件的處理器關聯起來。按照下面所給方法修改例項程式碼,同樣可以使用自定義的路由事件。

(1)註釋掉以下四行程式碼。

this.Grid_ThirdLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));       
this.Grid_SecondLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));            
this.Grid_FirstLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));           
this.Button_Confirm.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));

(2)在XAML程式碼中將監聽的事件與事件的處理器關聯起來,具體操作見下圖說明。

 

圖2 在XAML中將監聽的事件與事件處理器關聯

(3)圖2顯示,此時使用的事件處理器不再是Button_Clicked1,而是Button_Clicked2,下面給出Button_Clicked2的程式碼。

private void Button_Clicked2(object sender, DetailReportEventArgs e)
{
    FrameworkElement ele = sender as FrameworkElement;
    MessageBox.Show(e.EventPublisher + "-->" + ele.Name + ";" + e.EventTime);
}

將Button_Clicked2與Button_Clicked1進行對比,發現兩者的本質區別就是:第二個引數的型別不相同。

Button_Clicked2的第二個引數的型別為DetailReportEventArgs,而Button_Clicked1的第二個引數的型別為RoutedEventArgs。

繼續Q:造成這種區別的原因又是什麼呢?

那是因為在使用AddHandler方法時,傳入的事件處理器必須滿足RoutedEventHandler的定義,而RoutedEventHandler的定義如下所示:

public delegate void RoutedEventHandler(object sender, RoutedEventArgs e)

很顯然,RoutedEventHandler本質上就是一個委託,而與該委託能夠匹配的方法必須滿足:

第一個引數為object型別,第二個引數為RoutedEventArgs型別,且不具有返回值。

所以例項中使用了滿足該委託要求的Button_Clicked1。

接著闡述,Button_Clicked2存在的原因。

檢視自定義路由事件的定義程式碼:

public static readonly RoutedEvent DetailReportEvent = EventManager.RegisterRoutedEvent("DetailReport",RoutingStrategy.Bubble,typeof(EventHandler<DetailReportEventArgs>),typeof(DetailReportButton));

從定義中可以找到路由事件要求的事件處理器的型別,為EventHandler<DetailReportEventArgs>。

同樣可以檢視EventHandler<DetailReportEventArgs>的定義,如下所示:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

將其中的TEventArgs替換成DetailReportEventArgs,得到以下定義程式碼:

public delegate void EventHandler<DetailReportEventArgs>(object sender, DetailReportEventArgs e);

看到這個定義,我想大家也就知道了Button_Clicked2存在的原因了吧。

Q2: 既然為路由事件增加了CLR事件包裝,那麼,我們是否可以通過“+=”與“-=”符號來操作路由事件?

當然可以。

下面將通過修改示例程式碼進行演示來說明這一點。

註釋掉例項中一切將事件與事件處理器關聯起來的程式碼(畫面程式碼以及畫面後端程式碼中均要這麼做)。

在畫面後端程式碼中增加以下程式碼:

this.Button_Confirm.DetailReport += Button_Clicked1;

執行程式,點選按鈕,得到以下訊息框:

 

圖3 使用“+=”訂閱事件後的執行效果圖

事件處理器被正確執行,此時的路由事件與普通的CLR事件相同,直接將訊息傳遞給繫結的事件處理器。

接著,再增加以下程式碼:

this.Button_Confirm.DetailReport -= Button_Clicked1;

再次執行程式,點選按鈕,這下子,就“呵呵”了,我想你懂的。

Q3 :同一容器中的控制元件之間能夠相互監聽路由事件的訊息嗎?

按照下面的描述修改示例程式碼。

(1)在DetailReportButton按鈕前增加名為Button_SameLayer的按鈕。

<Grid x:Name="Grid_ThirdLayer" >
    <Button x:Name="Button_SameLayer"/>
    <local:DetailReportButton x:Name="Button_Confirm"  Width="100" Height="100" Content="Click Me" Margin="142,111,250,100" />
</Grid>

(2)使Button_SameLayer監聽DetailReportButton的事件DetailReportEvent。           

 this.Button_SameLayer.AddHandler(DetailReportButton.DetailReportEvent, new RoutedEventHandler(Button_Clicked1));

(3)執行程式,發現Button_SameLayer並沒有監聽到路由事件的到來。

由此可見,同一容器中的控制元件之間無法相互監聽路由事件的訊息。

Q4:如何讓一個路由事件傳遞至某一個節點處不再繼續傳遞?

在事件處理方法Button_Clicked1中,增加以下程式碼:

if (ele == this.Grid_SecondLayer)
{
    e.Handled = true;
}
這樣一來,當訊息傳遞至Grid_SecondLayer控制元件後將不會繼續傳遞下去。

因為,“e.Handled= true”的意思是說:該路由事件已經被處理了,不需要再繼續傳遞下去。

Q5: RoutedEventArgs的Source與OriginalSource的區別是什麼?

為了闡述這個主題,需要對例項做相對較大的一次變動。

(1)新建使用者控制元件MyUserControl,MyUserControl不完成任何有意義的工作,MyUserControl控制元件的XAML程式碼如下所示:

<UserControl x:Class="WpfRoutedEventExp.MyUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid x:Name="Grid_FirstLayerInUserControl">
        <Grid x:Name="Grid_SecondLayerInUserControl">
            <Button x:Name="Button_ThirdLayerInUserControl" Width="100" Height="100" Content="Hello,Kitty!"/>
        </Grid>
    </Grid>
</UserControl>

程式碼對應的設計檢視如下圖所示:

 

圖4 自定義控制元件的設計圖

(2)在例項程式碼中使用我們自定義的使用者控制元件,併為其指定名稱為myUserControl。

<Grid x:Name="Grid_ThirdLayer" >
     <local:DetailReportButton x:Name="Button_Confirm" Width="100" Height="100" Content="Click Me" Margin="142,111,250,100" />            
     <local:MyUserControl x:Name="myUserControl" Width="100" Height="100" HorizontalAlignment="Left" Margin="285,110,0,100"/>  </Grid>
自定義控制元件myUserControl位於容器Grid_ThirdLayer之中,Button_Confirm按鈕之後。

 

圖5 使用自定義的控制元件

(2)去掉所有的事件監聽。

(3)使Grid_FirstLayer監聽Button的Click事件。

this.Grid_FirstLayer.AddHandler(Button.ClickEvent, new RoutedEventHandler(Button_Clicked1));

(4)修改事件處理器方法Button_Clicked1,在方法中輸出事件的Source與OriginalSource屬性值。

private void Button_Clicked1(object sender, RoutedEventArgs e)
{
   MessageBox.Show(e.Source + ";" + e.OriginalSource );
}
(5)點選“Hello,Kitty!”按鈕(“Hello,Kitty!”按鈕位於自定義控制元件myUserControl中),得到以下結果:

 

圖6 Source與OriginalSource的區別

從結果可以看到:e.Source的值為MyUserControl(即自定義控制元件),而e.OriginalSource的值為Button(即“Hello,Kitty!”按鈕)。

Source與OriginalSource均為事件源。只不過Source表示的是LogicalTree上的訊息源頭,而OriginalSource表示的是VisualTree上的源頭。

Button.Click路由事件是從自定義控制元件內部名為Button_ThirdLayerInUserControl的Button控制元件發出的,在主視窗中,myUserControl是LogicalTree的末端節點,所以e.Source就是MyUserControl;而窗體的VisualTree則包含了MyUserControl的內部結構,所以e.OriginalSource將返回Button_ThirdLayerInUserControl按鈕。

寫在後面

本文未完,以後待續...