WPF入门到跪下 第七章 事件

WPF的事件包括生命周期事件、输入事件、路由事件和行为等方面。

生命周期事件

当WPF程序运行时候,编译完成后会生成并调用一个程序入口函数,即Main函数,Main函数中会实例化App对象,并调用App对象的初始化函数和Run函数。

关于Main函数,可以在编译后查看程序集目录下obj\Debug文件夹中的App.g.i.cs文件。

一、应用事件

应用程序的生命周期事件可以在App.xaml.cs文件中的APP类的构造函数中进行定义。(没构造函数可以自己定义)

Startup:在调用Application对象的Run方法时发生。

SessionEnding:在用户通过注销或关闭操作系统结束Windows会话时发生。

Activated:当应用程序中任意窗口成为前台应用程序时发生,也就是应用窗体获得焦点时。

Deactivated:当应用程序中所有窗口停止作为前台应用程序时发生, 也就是应用程序完全失去焦点时。

Exit:在应用程序关闭之前发生,无法取消。

public App()
{
    Startup += MyStartUpFunc;
		......
}

二、Browser类型的应用事件(Page开发)

Navigating:在应用程序中的导航请求新导航时发生。

LoadCompleted:在已经加载、分析并开始呈现应用程序中的导航器导航到的内容时发生。

Navigated:在已经找到应用程序中的导航器要导航的内容时发生,尽管此时可内容可能尚未完成加载。

NavigatedFailed:在应用程序中的导航器在导航到所请求内容时出现错误的情况下发生。

NavigationProgress:在由应用程序中的导航器管理的下载过程中定期发生,以提供导航进度信息。

NavigationStopped:在调用程序中的导航器的StopLoading方法时发生,或者当导航器在当前导航正在进行期间请求了一个新导航时发生。

以上Browser事件,如果是做窗口开发基本上都用不上,其中Navigating事件会在应用启动时调用一次,来加载确认有没有Page,需不需要进行导航。需要使用事件时同样可以在App构造函数中订阅。

public App()
{
		......
    Navigating += NavigatingFunc;
		......
}

三、异常捕获事件

DispatcherUnhandledException:在应用程序UI线程引发异常但未进行处理时发生。

  • 注意,这里说的是UI线程上的异常,此事件无法捕获UI线程外的异常。

AppDomain.CurrentDomain.UnhandledException:应用程序上所有线程引发异常但未处理时发生。

  • 这个事件可以捕获除Task线程外的所有线程发生的异常,比DispatcherUnhandledException事件更加全面。

TaskScheduler.UnobservedTaskExceptionTask线程引发异常但没有进行处理时触发,专门捕获应用上的Task异常。

  • 需要注意的是这个事件的触发时机,此事件并不会在异常发生时就触发,要在#C进行垃圾回收后才会触发。触发时间有一定的不确定性。(可以过GC.Collect()主动进行垃圾回收)
public App()
{
		......
    DispatcherUnhandledException += MyExceptionFunc;
		......
}

窗体常见事件

窗体的生命周期事件可以在对应窗体的cs文件的窗体类型构造函数中进行定义。

SourceInitialized:操作系统给窗体分配句柄时触发。

  • 说到这里,提及一下,与Winform不同,在WPF中,只有窗口具有句柄,窗口中的控件上是没有的。

ContentRendered:对应窗体内容渲染时触发,一般在窗口首次呈现时触发。

Loaded:窗体加载完成时触发。

Activated:当前窗口成为前台应用程序时发生,也就是当前窗体获得焦点时触发。

Deactivated:当前窗体失去焦点时触发。

Closing:窗体关闭时触发。

Closed:窗体关闭后触发。

输入事件

一、鼠标输入事件

WPF中的常用鼠标事件有:MouseEnterMouseLeaveMouseDownMouseUpMouseMoveMouseLeftButtonDownMouseLeftButtonUpMouseRightButtonDownMouseRightButtonUpMouseDoubleClick

这些都是一些很常用的事件,使用上都差不多,没啥好说的。

MouseLeftButtonDown诡异事件

有一点需要注意的是,在使用时会发现MouseLeftButtonDown事件如果用在Button控件上是不会触发(目前只发现MouseLeftButtonDown事件这样而MouseLeftButtonDown不会触发也导致了MouseLeftButtonUp不会触发)的,原因有两点:

  • 第一,WPF设计原则上的问题,控件在捕获了MouseLeftButtonDown事件后,会将该事件的Handled设置为True,这个属性是用在事件路由中的,当某个控件得到一个RoutedEvent,就会检测Handled是否为true,为true则忽略该事件。
  • 控件本身的Click事件,相当于将MouseLeftButtonDown事件抑制了,转换成了Click事件。

对此有三种解决方案:

  • 第一:使用PreviewMouseLeftButtonDown事件来代替。(最简单了)
  • 第二:在路由中去进行对应的处理。
  • 第三:在初始化的函数里利用UIElementAddHandler方法,显式的增加这个事件。

二、键盘输入事件

KeyDownKeyUp:键盘按下和松开事件。

  • 可以通过事件函数的KeyEventArgs参数对象来获取实际按下的键盘信息。
  • System.Windows.Input命名控件下有枚举类型Keys,其中存放了与键盘上所有键对应的值。
private void Button_KeyDown(object sender, KeyEventArgs e)
{
    if (e.KeyCode == Keys.Enter)
    {
        ......
    }
}
  • 需要注意的是,WPF中,Window控件可以正常触发到PreviewKeyDown或者KeyDown事件,但是UserControl无法直接捕获到这两个事件,原因是UserConrtol默认是无法获取对焦的,因此如果希望在UserControl中触发这两个事件,触发之前先要给UserControl获取焦点。

    public partial class ComponentConfigDialog : UserControl
    {
        public ComponentConfigDialog()
        {
            InitializeComponent();
            //为了实现键盘的KeyDown事件,UserControl无法自动获取焦点,所以在这里获取
            Focusable = true;
            Focus();
        }
    }
    

TextInput:文本框输入事件,当文本输入时触发,但使用后发现跟MouseLeftButtonDown类似,无法触发。可以使用PreviewTextInput事件来代替。

除上述键盘输入事件外,常用的还有KeyPressEvent事件,一次键盘完整的按下松开时触发,需要注意的是此事件函数接收的不是KeyEventArgs对象而是KeyPressEventArgs对象,两者在用法上略有不同。

此外,在WPF中,如果在C#编写过程中希望获取当前键盘上按着的修饰键是什么,可以使用Keyboard.Modifiers

  • Keyboard.Modifiers:获取或设置当前正在按着的修饰键(例如Alt、Shift等),其有效值可以通过ModifierKeys枚举获取。

    if (Keyboard.Modifiers == ModifierKeys.Alt)
    {
    		......
    }
    

三、拖拽事件

拖拽接收事件

Drop:在输入系统报告出现将此元素作为放置目标的基础放置事件时发生。其实也就是将其他控件拖拽到当前控件时,当前控件的Drop事件就会触发。

  • 前提条件是要将当前控件设置为允许接收拖放,设置 AllowDrop=True
  • 作为收纳容器的控件,Background不能为null,例如Canvas默认就是null的,这时要设置Background="Transparent",否则拖拽的控件会放置到上一层有Background实例的控件上。

拖拽方法

DragDrop.DoDragDrop(DependencyObject dragSource, object data, DragDropEffects allowedEffects):启动控件的拖拽操作。

  • dragSource:拖拽的控件对象。
  • data:传递给收纳控件的Drop事件的处理函数的数据,为object类型。注意,不能为null,否则报错。
  • allowedEffects:拖拽的数据模式,为枚举类型。感觉没啥用,就是鼠标的显示略有不同,一般使用Copy或者Move就可以了。

拖拽接收事件的事件参数类型

DragEventArgs:拖拽事件的参数类型。

  • Source:接收拖放的容器控件对象。

  • Data:获取IDataObject数据对象。

  • GetPosition(IInputElement relativeTo):获取当前拖放位置与指定控件对象的相对坐标。

    • 也就是获取放开鼠标左键时,鼠标与relativeTo的相对位置,一般可以使用上述中的Source对象。
  • GetData(Type format)IDataObject对象的实例方法(注意这里是IDataObject的实例方法),获取传输数据中,指定数据类型的对象。

  • 完整示例

    xaml代码

    <Grid ShowGridLines="True">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Border Height="80" Width="100" Background="Blue" MouseDown="Border_MouseDown"/>
        <Canvas Grid.Column="1" AllowDrop="True" Drop="Canvas_Drop" Background="Transparent"/>
    </Grid>
    

    后台代码

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    
        private void Border_MouseDown(object sender, MouseButtonEventArgs e)
        {
            DragDrop.DoDragDrop(sender as DependencyObject, sender, DragDropEffects.Move
        }
    
        private void Canvas_Drop(object sender, DragEventArgs e)
        {
            var receiver = e.Source as Canvas;
            var point = e.GetPosition(e.Source as IInputElement);
    
            var border = (Border)e.Data.GetData(typeof(Border));
            var newBorder = new Border();
            newBorder.Width = border.Width;
            newBorder.Height = border.Height;
            newBorder.Background = border.Background;
            receiver.Children.Add(newBorder);
            Canvas.SetTop(newBorder, point.Y - newBorder.Height/2);
            Canvas.SetLeft(newBorder, point.X - newBorder.Width/2);
        }
    }
    

这里可能会产生疑问,就是能不能直接将拖拽的控件作为数据传给收纳控件的Drop事件函数,然后将其直接作为子类添加到收纳控件?看起来像是可以,然而实际上因为一个控件对象不能同时存在于两个控件容器中,所以不能这么处理,一般的做法是根据传递给收纳控件的数据,通过反射来创建一个新的控件实例然后添加到收纳控件中,或者根据现有的数据集合,给集合添加数据。

行为

行为可以看作是对一系列事件的封装,可以在行为类型中,对使用了行为的控件进行指定事件的订阅以及事件处理函数的定义。

行为并不是WPF中的核心部分,是Expression Blend的设计特性,可以用触发器来取代行为。当然了,行为使用起来还是比较方便的。

想要使用行为,首先要通过Nuget下载对应的库Microsoft.Xaml.Behaviors

在这里插入图片描述

一、Behavior

行为的使用其实就是对Behavior<T>的使用,创建行为类型必须继承Behavior<T>

Behavior<T>中有几个必须用到的成员,分别为OnAttached()函数、OnDetaching()函数和AssociatedObject属性。

AssociatedObjectBehavior的属性成员,为当前使用行为的控件对象。

OnAttached():当WPF应用程序挂载使用了当前行为的控件对象时调用,一般用来给对应控件对象进行事件的订阅。

OnDetaching():当对应的控件对象销毁时调用,一般用来给控件对象取消事件的订阅,以减小对资源的占用。

public class BorderMoveBehavior : Behavior<Border>
{
    protected override void OnAttached()
    {
        base.OnAttached();

        //给控件对象进行多个事件的订阅,这里随便写一个意思一下
        AssociatedObject.MouseDown += Method;
    }

    private void Method(object sender, MouseButtonEventArgs e)
    {
        //随便干点啥
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        //给订阅的事件全部取消订阅
        AssociatedObject.MouseDown -= Method;
    }
}

二、行为实例

这里以Border控件的拖动行为为例进行学习。

创建行为类型

创建类型继承Behavior<T>,然后重写OnAttached()函数、OnDetaching()函数。在函数中使用AssociatedObject属性给控件对象进行事件订阅和取消订阅。

在本例中,逻辑编写过程中有一点需要注意的是鼠标的锁定问题:

  • 当鼠标按下时必须将鼠标锁定到当前控件对象,否则当鼠标移动过快或者与碰撞物相接时,控件对象容易失去鼠标焦点。

  • 当鼠标松开时,必须将鼠标解锁。

  • Mouse.Capture(IInputElement element[, CaptureMode captureMode]):将鼠标对象锁定到指定的对象上,等价于obj.CaptureMouse()

    • captureMode:捕获模式,默认为CaptureMode.Element

      CaptureMode.Element1鼠标捕获应用于单个元素。 鼠标输入转至已捕获的元素。
      CaptureMode.SubTree2鼠标捕获应用于元素的子树。 如果鼠标悬停在具有捕获的元素的子级上,则会将鼠标输入发送至该子元素。 否则,会将鼠标输入发送至具有鼠标捕获的元素。
      CaptureMode.None0没有鼠标捕获。 鼠标输入转至鼠标下的元素。
  • Mouse.Capture(null):将鼠标对象从当前锁定对象上解除,等价于obj.ReleaseMouseCapture()

  • 具体代码

    
    public class BorderMoveBehavior : Behavior<Border>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
    
            //给控件对象进行多个事件的订阅,这里随便写一个意思一下
            AssociatedObject.MouseLeftButtonDown += MouseDownMethod;
            AssociatedObject.MouseLeftButtonUp += MouseUpMethod;
            AssociatedObject.MouseMove += MouseMoveMethod;
        }
    
        //父类的Canvas容器对象
        private Canvas parentCanvas = null;
        private bool isDragging = false;
        private Point mouseCurrentPoint;
        private void MouseMoveMethod(object sender, MouseEventArgs e)
        {
            if (isDragging)
            {
                // 相对于Canvas的坐标
                Point point = e.GetPosition(parentCanvas);
                // 设置最新坐标
                AssociatedObject.SetValue(Canvas.TopProperty, point.Y - mouseCurrentPoint.Y);
                AssociatedObject.SetValue(Canvas.LeftProperty, point.X - mouseCurrentPoint.X);
            }
        }
    
        private void MouseUpMethod(object sender, MouseButtonEventArgs e)
        {
            if (isDragging)
            {
          
                isDragging = false;
                //锁定鼠标到当前控件对象
                //AssociatedObject.CaptureMouse();
                Mouse.Capture(null);
            }
        }
    
        private void MouseDownMethod(object sender, MouseButtonEventArgs e)
        {
    
            isDragging = true;
            // Canvas
            if (parentCanvas == null)
                parentCanvas = (Canvas)VisualTreeHelper.GetParent(sender as Border);
            // 当前鼠标坐标
            mouseCurrentPoint = e.GetPosition(sender as Border);
            // 鼠标锁定
            //AssociatedObject.CaptureMouse();
            Mouse.Capture(AssociatedObject);
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            //给订阅的事件全部取消订阅
            AssociatedObject.MouseLeftButtonDown -= MouseDownMethod;
            AssociatedObject.MouseLeftButtonUp -= MouseUpMethod;
            AssociatedObject.MouseMove -= MouseMoveMethod;
        }
    }
    

在xaml中使用行为

<Window ......
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        ......>
    <Grid>
        <Canvas>
            <Border Background="Orange" Width="100" Height="100">
                <i:Interaction.Behaviors>
                    <local:BorderMoveBehavior/>
                </i:Interaction.Behaviors>
            </Border>
            <Border Background="Green" Width="100" Height="100" Canvas.Left="100" Canvas.Top="100">
                <i:Interaction.Behaviors>
                    <local:BorderMoveBehavior/>
                </i:Interaction.Behaviors>
            </Border>
        </Canvas>
    </Grid>
</Window>

路由事件

一、逻辑树与可视树

逻辑树

简单的说,逻辑树就是我们在XAML中进行开发时,那些具有“实体”的控件元素所组成的逻辑层次。由开发过程中所关注的那些界面布局或控件元素组成。

<Window ......>
    <Grid>
        <DockPanel>
            <Button/>
        </DockPanel>
    </Grid>
</Window>

如上述xaml代码所示,WindowGridDockPanelButton这几个元素一起组成了逻辑树。

可视树

可视树是由界面上可见的元素构成的,这些元素主要是由从Visual或者Visual3D类中派生出来的类。例如上面的代码中、这些组成逻辑树的元素本身还包含了一些由Visual或者Visual3D类派生出的一些可视树的元素。这些组成逻辑树的元素以及其所包含的可视元素全部一起组成了界面的可视树。

遍历逻辑树与可视树

LogicalTreeHelper:逻辑树的工具类,可以通过这个静态类对逻辑树进行操作。

VisualTreeHelper:可视树的工具类,可以通过这个静态类对可视树进行操作。

  • 遍历代码 (PS:这段代码来自网络文章,为了方便直接复制借鉴,后期再做改良)
    • 创建类型定义遍历逻辑

      public class WpfTreeHelper
      {
          static string GetTypeDescription(object obj)
          {
              return obj.GetType().FullName;
          }
      
          /// <summary>
          /// 获取逻辑树
          /// </summary>
          /// <param name="obj"></param>
          /// <returns></returns>
          public static TreeViewItem GetLogicTree(DependencyObject obj)
          {
              if (obj == null)
              {
                  return null;
              }
              //创建逻辑树的节点
              TreeViewItem treeItem = new TreeViewItem { Header = GetTypeDescription(obj), IsExpanded = true };
      
              //循环遍历,获取逻辑树的所有子节点
              foreach (var child in LogicalTreeHelper.GetChildren(obj))
              {
                  //递归调用
                  var item = GetLogicTree(child as DependencyObject);
                  if (item != null)
                  {
                      treeItem.Items.Add(item);
                  }
              }
      
              return treeItem;
          }
      
          /// <summary>
          /// 获取可视树
          /// </summary>
          /// <param name="obj"></param>
          /// <returns></returns>
          public static TreeViewItem GetVisualTree(DependencyObject obj)
          {
              if (obj == null)
              {
                  return null;
              }
      
              TreeViewItem treeItem = new TreeViewItem() { Header = GetTypeDescription(obj), IsExpanded = true };
      
              for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
              {
                  var child = VisualTreeHelper.GetChild(obj, i);
                  var item = GetVisualTree(child);
                  if (item != null)
                  {
                      treeItem.Items.Add(item);
                  }
              }
      
              return treeItem;
          }
      }
      
    • xaml界面设计

      <Window x:Class="WpfApp5.MainWindow"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              mc:Ignorable="d"
              Title="MainWindow" Height="450" Width="800">
          <Grid>
              <DockPanel>
                  <Button DockPanel.Dock="Top" Click="Button_Click" Content="获取逻辑树和可视树"></Button>
                  <Grid>
                      <Grid.ColumnDefinitions>
                          <ColumnDefinition></ColumnDefinition>
                          <ColumnDefinition Width="*"></ColumnDefinition>
                      </Grid.ColumnDefinitions>
                      <DockPanel Grid.Column="0">
                          <TextBlock DockPanel.Dock="Top" Text="逻辑树"></TextBlock>
                          <TreeView Name="tvLogicTree"></TreeView>
                      </DockPanel>
                      <DockPanel Grid.Column="1">
                          <TextBlock DockPanel.Dock="Top" Text="可视树"></TextBlock>
                          <TreeView Name="tvVisualTree"></TreeView>
                      </DockPanel>
                  </Grid>
              </DockPanel>
          </Grid>
      </Window>
      
    • Button事件代码

      public partial class MainWindow : Window
       {
           public MainWindow()
           {
               InitializeComponent();
           }
      
           private void Button_Click(object sender, RoutedEventArgs e)
           {
               tvLogicTree.Items.Add(WpfTreeHelper.GetLogicTree(this));
               tvVisualTree.Items.Add(WpfTreeHelper.GetVisualTree(this));
           }
      
       }
      

二、冒泡与隧道

WPF的事件都是由Window对象接收并然后在视觉树上逐层传递到对应控件上的。

Windows系统的消息都是通过句柄来传递的,而WPF中,只有窗口对象拥有句柄,这也是为什么会从Window对象开始隧道冒泡返回,最后响应Windows系统。

在这里延申出了两个概念:冒泡与隧道。

<Window
		...... 
		ButtonBase.Click="Window_Click">
    <Grid Background="Yellow" MouseLeftButtonDown="Grid_MouseLeftButtonDown" PreviewMouseLeftButtonDown="Grid_PreviewMouseLeftButtonDown">
        <StackPanel Background="Red" Height="300" Width=" 300" ButtonBase.Click="StackPanel_Click">
            <Border Margin="30" Background="Blue" Height="100" Width="100" MouseLeftButtonDown="Border_MouseLeftButtonDown"/>
            <Button Margin="30" Background="LightBlue" Height="100" Width="100" Click="Button_Click"/>
        </StackPanel>
    </Grid>
</Window>

在这里插入图片描述

点击Button
在这里插入图片描述
点击Border
在这里插入图片描述

冒泡与隧道

查看上述代码,在WPF中,当点击Button时,一个完整的事件传递过程应该是从1-7的。

其中1-4是从Window接收到鼠标点击消息后逐层向下传递到目标控件对象Button上,这个过程称为隧道。

Button触发事件后、会将事件消息原路向上传递,即4-7的过程,这个过程称之为冒泡

事件优先级

从Debug输出台中打印出来的信息中可以知道,在事件消息传递的过程中,有两点需要注意:

  • 第一点是,如果是同样的事件,默认情况下,引起消息传递的目标控件的事件优先级比其他控件的优先级要高,其他控件的事件只能等目标控件触发完事件之后才能触发,也就是要等到冒泡阶段沿路上的同样事件才会触发。
  • 第二点是,Preview事件可以无视第一点,在隧道中只要遇到对应事件,就可以先触发,并且不影响其他控件的事件。

基本上,Preview事件是在隧道中发生的,称之为预览事件(隧道事件);其他路由事件是在冒泡中触发的,称为冒泡事件。

注意,上面的示例中,如果Button没有设置Background属性,那么在鼠标点击时除了自身控件会触发点击事件,父类容器也会触发对应事件。

三、控制事件的触发

1、阻止消息的传递

在开发过程中,如果希望事件消息在某个事件触发之后,路径上的对应事件不再触发,可以在节点事件函数中,将Handled属性设置为true

WPF中,所有的路由事件都共享一个公共事件基类RoutedEventArgs,该类中定义了有Handled属性,Handle属性用于告诉消息系统,这个事件是否已经被处理,如果是,则后续事件不需要再对该消息进行处理。

private void Button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("Button_Click");
    e.Handled = true;
}

在这里插入图片描述

需要注意的是,仅仅是后续事件不再触发,消息依然在传递,这一点下面会讲到。

2、强制消息触发

如果再将Handled属性设置为true之后,又希望在后面的传播路径上的某个控件的对应事件在接收到消息后依然能够触发,可以通过显示添加事件的方式进行设置。

AddHandler(RoutedEvent routedEvent, Delegate handler, bool handledEventsToo):为调用对象添加路由事件,该方法为UIElement定义的方法,而几乎所有的控件都继承了UIElement,因此都可以通过该方法进行路由事件的添加。

  • routedEvent:具体的事件类型。
  • handler:事件的处理函数,通常会通过创建RoutedEventHandler对象来传入。
  • handledEventsToo:是否不论事件是否已经处理完成,仍要处理该事件。(即无视Handled
public MainWindow()
{
    InitializeComponent();
    Stack.AddHandler(Button.ClickEvent, new RoutedEventHandler(Force_StackPanel_Click), true);
}

在这里插入图片描述
可以看到,虽然在ButtonClick事件中已经将Handled设置为trueStackPanel的事件还是照样触发了。(这也说明了事件消息是正常传递的,只不过默认情况下遇到消息中的Handletrue时,事件就不触发了)

自定义路由事件

一、路由事件的定义

在WPF中,事件的定义需要两个要素:事件注册事件包装

1、事件的注册

事件注册通过EventManager类的静态方法RegisterRoutedEvent来实现。

RoutedEvent RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, Type handlerType, Type ownerType):注册路由事件。

  • name:事件名称。
  • routingStrategy:路由策略,为RoutingStrategy枚举类型。
    • RoutingStrategy.Tunnel:隧道触发
    • RoutingStrategy.Bubble:冒泡触发
    • RoutingStrategy.Direct:路由事件不通过元素树路由,但支持其他路由的功能事件,如EventTrigerEventSetter
  • handlerType:事件类型,路由事件就是typeof(RoutedEventHandler),属性变化事件则可以使用typeof(RoutedPropertyChangedEventHandler<T>)
  • ownerType:当前注册事件的拥有者的对应类型,例如typeof(MyEventControl)
public static readonly RoutedEvent ButtonClickedEvet =
    EventManager.RegisterRoutedEvent("ButtonClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(EventTestControl));

2、事件的包装

事件的包装跟依赖属性的包装类似,创建事件属性(注意这里不是getset,而是addremove),不过使用的是AddHandlerRemoveHandler方法。

需要注意的是,与依赖属性一样,不要在addremove块中添加除AddHandlerRemoveHandler以外的代码,这些代码并不会被执行。

public event RoutedEventHandler ButtonClicked
{
    add { AddHandler(ButtonClickedEvet, value); }
    remove { RemoveHandler(ButtonClickedEvet, value); }
}

二、事件的触发

自定义路由事件的触发,需要调用UIElement中定义的RaiseEvent方法。

RaiseEvent(RoutedEventArgs e):引发指定的路由事件。

  • e:路由事件参数类型对象,该对象包含了与路由事件相关联的状态信息和事件数据。

RoutedEventArgs(RoutedEvent routedEvent):根据指定的路由事件,创建路由事件参数RoutedEventArgs对象。

RoutedPropertyChangedEventArgs<T>(T oldValue, T newValue, RoutedEvent routedEvent):创建路由事件参数对象,可以携带两个值,配合依赖属性变化事件使用。

//用户控件的xaml中的某个按钮元素的事件绑定到此方法
private void Button_Click(object sender, RoutedEventArgs e)
{
    RoutedEventArgs args = new RoutedEventArgs(ButtonClickedEvet);
    RaiseEvent(args);
}

三、完整的示例

注意,这个示例是在Prism框架下使用的,在MainWindow程序集中添加了一个用户控件EventTestControl。

EventTestControl.xaml

<UserControl x:Class="WpfControlLibrary.EventTestControl"
             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" 
             xmlns:local="clr-namespace:WpfControlLibrary"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Button Click="Button_Click" Content="TestButton"/>
    </Grid>
</UserControl>

EventTestControl.xaml.cs

public partial class EventTestControl : UserControl
{
    public EventTestControl()
    {
        InitializeComponent();
    }

    public int Value
    {
        get { return (int)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(int), typeof(EventTestControl), new PropertyMetadata(0,ValuePropertyChangedCallback));

    private static void ValuePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is object && d is EventTestControl)
        {
            EventTestControl etc = d as EventTestControl;
            etc.OnValueChenged((int)e.OldValue, (int)e.NewValue);
        }
    }

    protected void OnValueChenged(int oldValue, int newValue)
    {
        RoutedPropertyChangedEventArgs<int> args = new RoutedPropertyChangedEventArgs<int>(oldValue, newValue, ValueChangedEvent);
        RaiseEvent(args);
    }

    //定义路由事件-Value属性变化事件
    public static readonly RoutedEvent ValueChangedEvent = 
        EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<int>), typeof(EventTestControl));
    public event RoutedPropertyChangedEventHandler<int> ValueChanged
    {
        add { AddHandler(ValueChangedEvent, value); }
        remove { RemoveHandler(ValueChangedEvent, value); }
    }

    //定义路由事件-用户控件中的某个按钮点击时触发
    public static readonly RoutedEvent ButtonClickedEvet =
        EventManager.RegisterRoutedEvent("ButtonClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(EventTestControl));
    public event RoutedEventHandler ButtonClicked
    {
        add { AddHandler(ButtonClickedEvet, value); }
        remove { RemoveHandler(ButtonClickedEvet, value); }
    }

    //用户控件中的某个按钮的事件绑定到此方法
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        RoutedEventArgs args = new RoutedEventArgs(ButtonClickedEvet);
        RaiseEvent(args);
    }
}

MainWindowViewModel.cs

public class MainWindowViewModel:BindableBase
{
	private int _value;

	public int Value
	{
		get { return _value; }
		set { SetProperty(ref _value, value);}
	}
}

MainWindow.xaml

<Grid>
    <Slider Maximum="100" Value="{Binding Value}"/>
    <local:EventTestControl Width="100" Height="50"
        ButtonClicked="EventTestControl_ButtonClicked" 
        ValueChanged="EventTestControl_ValueChanged"
        Value="{Binding Value}"/>
</Grid>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void EventTestControl_ButtonClicked(object sender, RoutedEventArgs e)
    {
        Debug.Print("button clicked");
    }

    private void EventTestControl_ValueChanged(object sender, RoutedPropertyChangedEventArgs<int> e)
    {
        Debug.Print(e.NewValue.ToString());
    }
}

  • 28
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SchuylerEX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值