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.UnobservedTaskException:Task线程引发异常但没有进行处理时触发,专门捕获应用上的Task异常。
需要注意的是这个事件的触发时机,此事件并不会在异常发生时就触发,要在#C进行垃圾回收后才会触发。触发时间有一定的不确定性。(可以过GC.Collect()主动进行垃圾回收)
public App()
{
......
DispatcherUnhandledException += MyExceptionFunc;
......
}
窗体常见事件
窗体的生命周期事件可以在对应窗体的cs文件的窗体类型构造函数中进行定义。
SourceInitialized:操作系统给窗体分配句柄时触发。
说到这里,提及一下,与Winform不同,在WPF中,只有窗口具有句柄,窗口中的控件上是没有的。
ContentRendered:对应窗体内容渲染时触发,一般在窗口首次呈现时触发。
Loaded:窗体加载完成时触发。
Activated:当前窗口成为前台应用程序时发生,也就是当前窗体获得焦点时触发。
Deactivated:当前窗体失去焦点时触发。
Closing:窗体关闭时触发。
Closed:窗体关闭后触发。
输入事件
一、鼠标输入事件
WPF中的常用鼠标事件有:MouseEnter、MouseLeave、MouseDown、MouseUp、MouseMove、MouseLeftButtonDown、MouseLeftButtonUp、MouseRightButtonDown、MouseRightButtonUp、MouseDoubleClick
这些都是一些很常用的事件,使用上都差不多,没啥好说的。
MouseLeftButtonDown诡异事件
有一点需要注意的是,在使用时会发现MouseLeftButtonDown事件如果用在Button控件上是不会触发(目前只发现MouseLeftButtonDown事件这样而MouseLeftButtonDown不会触发也导致了MouseLeftButtonUp不会触发)的,原因有两点:
第一,WPF设计原则上的问题,控件在捕获了MouseLeftButtonDown事件后,会将该事件的Handled设置为True,这个属性是用在事件路由中的,当某个控件得到一个RoutedEvent,就会检测Handled是否为true,为true则忽略该事件。控件本身的Click事件,相当于将MouseLeftButtonDown事件抑制了,转换成了Click事件。
对此有三种解决方案:
第一:使用PreviewMouseLeftButtonDown事件来代替。(最简单了)第二:在路由中去进行对应的处理。第三:在初始化的函数里利用UIElement的AddHandler方法,显式的增加这个事件。
二、键盘输入事件
KeyDown、KeyUp:键盘按下和松开事件。
可以通过事件函数的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代码
后台代码
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
Behavior
AssociatedObject:Behavior的属性成员,为当前使用行为的控件对象。
OnAttached():当WPF应用程序挂载使用了当前行为的控件对象时调用,一般用来给对应控件对象进行事件的订阅。
OnDetaching():当对应的控件对象销毁时调用,一般用来给控件对象取消事件的订阅,以减小对资源的占用。
public class BorderMoveBehavior : Behavior
{
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
在本例中,逻辑编写过程中有一点需要注意的是鼠标的锁定问题:
当鼠标按下时必须将鼠标锁定到当前控件对象,否则当鼠标移动过快或者与碰撞物相接时,控件对象容易失去鼠标焦点。
当鼠标松开时,必须将鼠标解锁。
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
{
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中使用行为
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" ......>
路由事件
一、逻辑树与可视树
逻辑树
简单的说,逻辑树就是我们在XAML中进行开发时,那些具有“实体”的控件元素所组成的逻辑层次。由开发过程中所关注的那些界面布局或控件元素组成。
如上述xaml代码所示,Window、Grid、DockPanel、Button这几个元素一起组成了逻辑树。
可视树
可视树是由界面上可见的元素构成的,这些元素主要是由从Visual或者Visual3D类中派生出来的类。例如上面的代码中、这些组成逻辑树的元素本身还包含了一些由Visual或者Visual3D类派生出的一些可视树的元素。这些组成逻辑树的元素以及其所包含的可视元素全部一起组成了界面的可视树。
遍历逻辑树与可视树
LogicalTreeHelper:逻辑树的工具类,可以通过这个静态类对逻辑树进行操作。
VisualTreeHelper:可视树的工具类,可以通过这个静态类对可视树进行操作。
遍历代码 (PS:这段代码来自网络文章,为了方便直接复制借鉴,后期再做改良)
创建类型定义遍历逻辑
public class WpfTreeHelper
{
static string GetTypeDescription(object obj)
{
return obj.GetType().FullName;
}
///
/// 获取逻辑树
///
///
///
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;
}
///
/// 获取可视树
///
///
///
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界面设计
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">
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系统。
在这里延申出了两个概念:冒泡与隧道。
...... ButtonBase.Click="Window_Click">
点击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);
}
可以看到,虽然在Button的Click事件中已经将Handled设置为true,StackPanel的事件还是照样触发了。(这也说明了事件消息是正常传递的,只不过默认情况下遇到消息中的Handle为true时,事件就不触发了)
自定义路由事件
一、路由事件的定义
在WPF中,事件的定义需要两个要素:事件注册和事件包装。
1、事件的注册
事件注册通过EventManager类的静态方法RegisterRoutedEvent来实现。
RoutedEvent RegisterRoutedEvent(string name, RoutingStrategy routingStrategy, Type handlerType, Type ownerType):注册路由事件。
name:事件名称。routingStrategy:路由策略,为RoutingStrategy枚举类型。
RoutingStrategy.Tunnel:隧道触发RoutingStrategy.Bubble:冒泡触发RoutingStrategy.Direct:路由事件不通过元素树路由,但支持其他路由的功能事件,如EventTriger、EventSetter。
handlerType:事件类型,路由事件就是typeof(RoutedEventHandler),属性变化事件则可以使用typeof(RoutedPropertyChangedEventHandler
public static readonly RoutedEvent ButtonClickedEvet =
EventManager.RegisterRoutedEvent("ButtonClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(EventTestControl));
2、事件的包装
事件的包装跟依赖属性的包装类似,创建事件属性(注意这里不是get、set,而是add、remove),不过使用的是AddHandler和RemoveHandler方法。
需要注意的是,与依赖属性一样,不要在add与remove块中添加除AddHandler与RemoveHandler以外的代码,这些代码并不会被执行。
public event RoutedEventHandler ButtonClicked
{
add { AddHandler(ButtonClickedEvet, value); }
remove { RemoveHandler(ButtonClickedEvet, value); }
}
二、事件的触发
自定义路由事件的触发,需要调用UIElement中定义的RaiseEvent方法。
RaiseEvent(RoutedEventArgs e):引发指定的路由事件。
e:路由事件参数类型对象,该对象包含了与路由事件相关联的状态信息和事件数据。
RoutedEventArgs(RoutedEvent routedEvent):根据指定的路由事件,创建路由事件参数RoutedEventArgs对象。
RoutedPropertyChangedEventArgs
//用户控件的xaml中的某个按钮元素的事件绑定到此方法
private void Button_Click(object sender, RoutedEventArgs e)
{
RoutedEventArgs args = new RoutedEventArgs(ButtonClickedEvet);
RaiseEvent(args);
}
三、完整的示例
注意,这个示例是在Prism框架下使用的,在MainWindow程序集中添加了一个用户控件EventTestControl。
EventTestControl.xaml
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">
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
RaiseEvent(args);
}
//定义路由事件-Value属性变化事件
public static readonly RoutedEvent ValueChangedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler
public event RoutedPropertyChangedEventHandler
{
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
ButtonClicked="EventTestControl_ButtonClicked" ValueChanged="EventTestControl_ValueChanged" Value="{Binding Value}"/> 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 { Debug.Print(e.NewValue.ToString()); } }