重要的应用程序接口(API)
在将 XAML 用于 UI 定义时,我们将介绍 Windows 运行时应用中事件的编程概念。 可以将事件的处理程序分配为 XAML 中 UI 元素的声明的一部分,也可以在代码中添加处理程序。 Windows 运行时支持 路由事件:某些输入事件和数据事件可由触发事件的对象以外的对象处理。 定义控件模板或使用页面或布局容器时,路由事件非常有用。
事件作为编程概念
一般来说,在编程 Windows 运行时应用时的事件概念与最常用的编程语言中的事件模型类似。 如果已了解如何使用 Microsoft .NET 或C++事件,则可以从头开始。 但是,你不需要了解太多事件模型的概念就可以执行一些基本任务,比如附加事件处理程序。
创建 WinUI(或 UWP)应用时,UI 在标记(XAML)中定义。 在 XAML 标记语法中,标记元素和运行时代码实体之间连接事件的一些原则与其他 Web 技术(如 ASP.NET 或 HTML5)类似。
注释
为 XAML 定义的 UI 提供运行时逻辑的代码通常称为 代码隐藏 或代码隐藏文件。 在 Microsoft Visual Studio 解决方案视图中,此关系以图形方式显示,代码隐藏文件是依赖和嵌套文件,而不是它引用的 XAML 页面。
“Button.Click”:事件与 XAML 简介
Windows 运行时应用的最常见编程任务之一是捕获 UI 的用户输入。 例如,UI 可能有一个按钮,用户必须单击该按钮才能提交信息或更改状态。
通过生成 XAML 为 Windows 运行时应用定义 UI。 此 XAML 通常是 Visual Studio 中设计图面的输出。 还可以在纯文本编辑器或第三方 XAML 编辑器中编写 XAML。 生成该 XAML 时,可以同时为单个 UI 元素连接事件处理程序,同时定义建立该 UI 元素的属性值的所有其他 XAML 属性。
若要在 XAML 中关联事件,请指定您已定义或稍后将在后台代码中定义的处理程序方法的名称(字符串格式)。 例如,此 XAML 定义了一个 Button 对象,其他属性(x:Name 属性 和 Content 属性)被分配为其属性,并通过引用一个名为 ShowUpdatesButton_Click 的方法来为按钮的 Click 事件 添加处理程序。
<Button x:Name="showUpdatesButton"
Content="{Binding ShowUpdatesText}"
Click="ShowUpdatesButton_Click"/>
小窍门
事件连接 是一个编程术语。 它指的是一个过程或代码, 通过该过程或代码指示事件发生时应调用命名的处理程序方法。 在大多数过程代码模型中,事件连接是隐式或显式的“AddHandler”代码,用于命名事件和方法,通常涉及目标对象实例。 在 XAML 中,“AddHandler”是隐式的,事件连接完全包括将事件命名为对象元素的属性名称,并将处理程序命名为该特性的值。
使用为您的应用程序所有代码和后端代码编写的编程语言编写具体处理程序。 使用特性 Click="ShowUpdatesButton_Click" 时,你创建了一个契约,当 XAML 进行标记编译和分析时,IDE 构建操作中的 XAML 标记编译步骤以及应用加载时的最终 XAML 解析,都可以在应用代码中找到命名为 ShowUpdatesButton_Click 的方法。
ShowUpdatesButton_Click 必须是实现 Click 事件的任何处理程序的兼容方法签名(基于委托)的方法。 例如,此代码定义 ShowUpdatesButton_Click 处理程序。
private void ShowUpdatesButton_Click (object sender, RoutedEventArgs e)
{
Button b = sender as Button;
//more logic to do here...
}
Private Sub ShowUpdatesButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim b As Button = CType(sender, Button)
' more logic to do here...
End Sub
void winrt::MyNamespace::implementation::BlankPage::ShowUpdatesButton_Click(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e)
{
auto b{ sender.as<Windows::UI::Xaml::Controls::Button>() };
// More logic to do here.
}
void MyNamespace::BlankPage::ShowUpdatesButton_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
{
Button^ b = (Button^) sender;
//more logic to do here...
}
在此示例中,ShowUpdatesButton_Click 方法基于 RoutedEventHandler 委托。 你会知道这是应该使用的委托,因为你会在 Click 方法的语法中看到这个委托的名称。
小窍门
Visual Studio 提供了一种在编辑 XAML 时命名事件处理程序和定义处理程序方法的便捷方法。 在 XAML 文本编辑器中提供事件的属性名称时,请稍等片刻,直到显示Microsoft IntelliSense 列表。 如果从列表中单击“新建事件处理程序<”>,Microsoft Visual Studio 会根据元素的 x:Name(或类型名称)、事件名称和数字后缀建议方法名称。 然后,可以右键单击所选事件处理程序名称,然后单击“ 导航到事件处理程序”。 这将直接导航到新插入的事件处理程序定义,如 XAML 页面的代码隐藏文件的代码编辑器视图中所示。 事件处理程序已具有正确的签名,包括 发送方 参数和事件使用的事件数据类。 此外,如果后台代码中已存在具有正确签名的处理程序方法,该方法的名称会显示在自动完成下拉框中,以及 <新建事件处理程序> 选项。 还可以按 Tab 键作为快捷方式,而不是单击 IntelliSense 列表项。
定义事件处理程序
对于作为 UI 元素并在 XAML 中声明的对象,事件处理程序代码是在作为 XAML 页面后台代码的部分类中定义的。 事件处理程序是作为与 XAML 关联的分部类的一部分编写的方法。 这些事件处理程序基于特定事件使用的委托。 事件处理程序方法可以是公共方法,也可以是专用方法。 专用访问有效,因为 XAML 创建的处理程序和实例最终由代码生成联接。 一般情况下,我们建议您在类中将事件处理器方法设为私有。
注释
C++的事件处理程序未在分部类中定义,它们在标头中声明为私有类成员。 C++项目的生成作业负责生成支持XAML类型系统和C++代码隐藏模型的代码。
发送方参数和事件数据
为事件编写的处理程序可以访问两个值,这些值可用作调用处理程序的每个情况的输入。 第一个此类值是 发送方,它是对附加处理程序的对象的引用。 发送方参数类型为基对象类型。 一种常见方法是将 发送方 强制转换为更精确的类型。 如果希望检查或更改 发送方 对象本身的状态,则此方法非常有用。 根据您自己的应用程序设计,您通常会了解一个适合将发送者安全转换为的类型,这取决于处理程序附加的位置或其他设计细节。
第二个值是事件数据,该数据通常以语法定义的形式显示为 e (或 args)参数。 可以通过查看为所处理的特定事件分配的委托的 e 参数,然后使用 Visual Studio 中的 IntelliSense 或对象浏览器来发现事件数据的哪些属性可用。 也可以使用 Windows 运行时参考文档。
对于某些事件,事件数据的特定属性值与知道事件发生一样重要。 输入事件尤其如此。 对于指针事件,事件发生时指针的位置可能很重要。 对于键盘事件,所有可能的按键都会触发 KeyDown 和 KeyUp 事件。 若要确定用户按下的键,必须访问可用于事件处理程序的 KeyRoutedEventArgs 。 有关处理输入事件的详细信息,请参阅 键盘交互 和 处理指针输入。 输入事件和输入方案通常具有本主题中未涵盖的其他注意事项,例如指针事件的指针捕获,以及键盘事件的修饰键和平台键代码。
使用 异步 模式的事件处理程序
在某些情况下,需要使用在事件处理程序中使用 异步 模式的 API。 例如,可以使用 AppBar 中的按钮来显示文件选取器并与之交互。 但是,许多文件选取器 API 都是异步的。 它们必须在 async/await 环境中调用,编译器将对此进行强制校验。 因此,你可以做的是将 async 关键字添加到事件处理器,以便处理程序变为 asyncvoid。 现在,您的事件处理器可以进行异步/可等待的调用。
在代码中添加事件处理程序
XAML 并不是将事件处理程序分配给对象的唯一方法。 若要将事件处理程序添加到代码中的任何给定对象(包括 XAML 中不可用的对象),可以使用特定于语言的语法添加事件处理程序。
在 C# 中,语法是使用 += 运算符。 通过引用运算符右侧的事件处理程序方法名称来注册处理程序。
如果使用代码将事件处理程序添加到运行时 UI 中显示的对象,常见做法是添加此类处理程序以响应对象生存期事件或回调(如 Loaded 或 OnApplyTemplate),以便相关对象的事件处理程序在运行时可供用户启动的事件使用。 此示例演示页面结构的 XAML 大纲,然后提供 C# 语言语法,用于向对象添加事件处理程序。
<Grid x:Name="LayoutRoot" Loaded="LayoutRoot_Loaded">
<StackPanel>
<TextBlock Name="textBlock1">Put the pointer over this text</TextBlock>
...
</StackPanel>
</Grid>
void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
textBlock1.PointerEntered += textBlock1_PointerEntered;
textBlock1.PointerExited += textBlock1_PointerExited;
}
注释
存在一种更冗长的语法。 2005 年,C# 添加了一个名为委托推理的功能,使编译器能够推断新的委托实例,并启用以前的更简单的语法。 冗长语法在功能上与上一个示例相同,但在注册之前显式地创建了一个新的委托实例,因此没有利用委托推理。 此显式语法不太常见,但仍可能在一些代码示例中看到它。
void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
textBlock1.PointerEntered += new PointerEventHandler(textBlock1_PointerEntered);
textBlock1.PointerExited += new MouseEventHandler(textBlock1_PointerExited);
}
Visual Basic 语法有两种可能性。 一种是将 C# 语法并行,并将处理程序直接附加到实例。 这需要 AddHandler 关键字和取消引用处理程序方法名称的 AddressOf 运算符。
Visual Basic 语法的另一个选项是对事件处理程序使用 Handles 关键字。 此方法适用于在加载时应存在于对象上的处理程序并在整个对象生存期内保留的情况。 对 XAML 中定义的对象使用句柄需要提供名称 / 。 此名称将成为 Handles 语法的 Instance.Event 部分所需的实例限定符。 在这种情况下,不需要基于对象生存期的事件处理程序来启动附加其他事件处理程序;编译 XAML 页时会创建 句柄 连接。
Private Sub textBlock1_PointerEntered(ByVal sender As Object, ByVal e As PointerRoutedEventArgs) Handles textBlock1.PointerEntered
' ...
End Sub
注释
Visual Studio 及其 XAML 设计图面通常提升实例处理技术,而不是 Handles 关键字。 这是因为在 XAML 中建立事件处理程序连接是典型的设计器开发人员工作流的一部分, 而 Handles 关键字技术与在 XAML 中连接事件处理程序不兼容。
在 C++/CX 中,也可以使用 += 语法,但与基本 C# 形式存在差异:
- 不存在委托推理,因此必须为委托实例使用 ref new 。
- 委托构造函数具有两个参数,需要目标对象作为第一个参数。 通常指定 此项。
- 委托构造函数需要方法地址作为第二个参数,因此方法名称前面需要 & 引用运算符。
textBlock1().PointerEntered({this, &MainPage::TextBlock1_PointerEntered });
textBlock1->PointerEntered +=
ref new PointerEventHandler(this, &BlankPage::textBlock1_PointerEntered);
删除代码中的事件处理程序
通常不需要删除代码中的事件处理程序,即使你在代码中添加了它们。 大多数 Windows 运行时对象(如页面和控件)的对象生存期行为会在对象与主 窗口 及其可视化树断开连接时销毁这些对象,并且任何委托引用也会销毁。 .NET 通过垃圾回收实现这一点,而 Windows 运行时环境中的 C++/CX 默认使用弱引用。
在某些罕见的情况下,你确实需要显式删除事件处理程序。 这些包括:
- 您为静态事件添加的处理程序,无法通过传统方式进行垃圾回收。 Windows 运行时 API 中的静态事件示例包括 CompositionTarget 和 剪贴板 类的事件。
- 你可以测试代码,以实现处理程序立即移除,或者在运行时对事件的旧/新处理程序进行交换。
- 自定义 删除 访问器的实现。
- 自定义静态事件。
- 页面导航的处理程序。
FrameworkElement.Unloaded 或 Page.NavigatedFrom 是在状态管理和对象生命周期中具有适当位置的事件触发器,这些触发器可以用于移除其他事件的事件处理器。
例如,可以使用此代码从目标对象 textBlock1 中删除名为 textBlock1_PointerEntered 的事件处理程序。
textBlock1.PointerEntered -= textBlock1_PointerEntered;
RemoveHandler textBlock1.PointerEntered, AddressOf textBlock1_PointerEntered
你还可以删除那些通过 XAML 属性添加的事件处理程序,这意味着这些处理程序是在生成的代码中被添加的。 如果为附加处理程序的元素提供了 Name 值,则这更容易实现,因为该元素稍后为代码提供对象引用;但是,还可以遍查对象树,以便在对象没有 名称的情况下查找必要的对象引用。
如果需要在 C++/CX 中删除事件处理程序,则您需要一个注册令牌,该令牌应该是从+=事件处理程序注册的返回值中获得的。 这是因为在 C++/CX 语法中,您在取消注册时用在右侧的值是令牌,而不是方法名称。 对于 C++/CX,无法删除已添加为 XAML 属性的处理程序,因为C++/CX 生成的代码不会保存令牌。
路由事件
Windows 运行时支持路由事件的概念,这个概念适用于大多数 UI 元素上的一组事件。 这些事件适用于输入和用户交互方案,它们是在 UIElement 基类上实现的。 下面是一些作为路由事件的输入事件列表:
- BringIntoViewRequested
- CharacterReceived(字符接收)
- ContextCanceled
- ContextRequested
- DoubleTapped
- DragEnter
- DragLeave
- DragOver
- DragStarting
- 删除
- 拖放完成
- GettingFocus
- GotFocus
- 控股
- KeyDown
- KeyUp
- LosingFocus
- LostFocus
- 操作完成
- ManipulationDelta
- ManipulationInertiaStarting
- 操控开始
- 操作开始
- NoFocusCandidateFound
- 指针取消
- PointerCaptureLost
- PointerEntered
- PointerExited
- PointerMoved
- PointerPressed
- PointerReleased
- PointerWheelChanged
- PreviewKeyDown
- PreviewKeyUp
- PointerWheelChanged
- RightTapped
- 窃听
路由事件是一个事件,该事件可能从子对象传递到对象树中的每个连续父对象(路由)。 UI 的 XAML 结构近似于此树,该树的根是 XAML 中的根元素。 真正的对象树可能与 XAML 元素嵌套稍有不同,因为对象树不包含 XAML 语言功能,如属性元素标记。 可以将路由事件视为从触发事件的任何 XAML 对象元素子元素向包含事件的父对象元素 浮出水泡 。 事件及其事件数据可以在事件路由中的多个对象上进行处理。 如果没有元素具有处理程序,则路由可能会持续到到达根元素为止。
如果你知道动态 HTML(DHTML)或 HTML5 等 Web 技术,你可能已经熟悉 浮泡 事件概念。
当路由事件在其事件路由上冒泡时,所有附加的事件处理程序都会访问共享的事件数据实例。 因此,如果任何事件数据可由处理程序写入,则对事件数据所做的任何更改都将传递到下一个处理程序,并且可能不再表示事件的原始事件数据。 当事件具有路由事件行为时,参考文档将包含有关路由行为的注释或其他表示法。
RoutedEventArgs 的 OriginalSource 属性
当事件沿着事件路由冒泡时,发送方 不再是引发事件的对象。 相反, 发送方 是正在调用的处理程序附加的对象。
在某些情况下,发送方并不是重点,而是更关注其他信息,例如当指针事件触发时,指针位于哪个可能的子对象上,或者当用户按下键盘键时,哪个对象在一个较大 UI 中拥有焦点。 对于这些情况,可以使用 OriginalSource 属性的值。 在路由的所有点, OriginalSource 报告触发事件的原始对象,而不是附加处理程序的对象。 但是,对于 UIElement 输入事件,原始对象通常是页面级 UI 定义 XAML 中不立即可见的对象。 相反,原始源对象可能是控件的模板化部分。 例如,如果用户将指针悬停在 Button 的边缘上,对于大多数指针事件,OriginalSource 是模板中的边框模板部件,而不是 Button 本身。
提示 如果要创建模板化控件,输入事件冒泡尤其有用。 具有模板的任何控件都可以由其使用者应用新模板。 尝试重新创建工作模板的使用者可能会无意中消除默认模板中声明的某些事件处理。 你仍然可以通过在类定义中附加处理程序作为 OnApplyTemplate 重写的一部分来提供控件级事件处理。 然后,可以捕获在实例化时冒泡到控件的根部的输入事件。
Handled 属性
特定路由事件的多个事件数据类包含名为 Handled 的属性。 有关示例,请参阅 PointerRoutedEventArgs.Handled、 KeyRoutedEventArgs.Handled、 DragEventArgs.Handled。 在所有情况下 ,Handled 都是一个可设置的布尔属性。
将 Handled 属性设置为 true 会影响事件系统行为。 如果Handled为true,那么大多数事件处理程序的路由将停止; 该事件不会继续沿路由传递,以通知其他附加处理程序关于该特定事件的情况。 “已处理”在事件上下文中的含义,以及你的应用如何响应它,均取决于你。 基本上,Handled 是一个简单的协议,它使得应用程序代码能够声明某事件发生时不需要向上传递到任何容器,因为您的应用逻辑已经处理了所需的操作。 不过,你确实需要小心,确保不去处理那些最好应当冒泡的事件,以便内置的系统或控件行为能够正常发挥作用。例如,在一个选择控件的部件或项目中处理底层事件可能会产生不利影响。 选择控件可能正在寻找输入事件,以了解所选内容应发生更改。
并非所有的路由事件都可以通过这种方式取消。你可以通过这一点判断,因为它们没有 Handled 属性。 例如, GotFocus 和 LostFocus 会冒泡,但它们始终一路气泡到根,并且其事件数据类没有可影响该行为的 Handled 属性。
控件中的输入事件处理程序
特定的 Windows 运行时控件有时在内部使用 处理 的概念来输入事件。 这可能导致输入事件从未发生,因为用户代码无法处理它。 例如, Button 类包括有意处理常规输入事件 PointerPressed 的逻辑。 这样做是因为按钮触发了一个Click事件,该事件由指针按下时的输入启动,同时也可以通过其他输入模式启动,例如处理像 Enter 键这样的按键,它们在按钮聚焦时能够调用按钮。 对于 Button 的类设计,原始输入事件在概念上得到处理,而类使用者(如用户代码)可以改为与与控件相关的 Click 事件交互。 Windows 运行时 API 参考中特定控件类的主题通常记下类实现的事件处理行为。 在某些情况下,可以通过重写 OnEvent 方法来更改行为。 例如,可以通过重写 Control.OnKeyDown 来更改 TextBox 派生类对键输入的反应。
为已处理的路由事件注册处理程序
早些时候,我们说,将 Handled 设置为 true 可防止调用大多数处理程序。 但 AddHandler 方法提供了一种技术,你可以在其中附加始终为路由调用的处理程序,即使路由前面的某些其他处理程序在共享事件数据中将 Handled 设置为 true 。 如果正在使用的控件在其内部组合中或特定于控件的逻辑中处理了该事件,则此方法非常有用。 但你仍希望从控件实例或应用程序用户界面响应它。 但请谨慎使用此技术,因为它可能与 Handled 的目的相矛盾,并可能破坏控件的预期交互。
只有具有相应路由事件标识符的路由事件才能使用 AddHandler 事件处理技术,因为该标识符是 AddHandler 方法的必需输入。 请参阅 AddHandler 的参考文档以获取提供路由事件标识符的事件列表。 大体上,这是我们之前展示给您的路由事件列表。 例外是列表中的最后两个: GotFocus 和 LostFocus 没有路由事件标识符,因此不能为这些标识符使用 AddHandler 。
可视化树外部的路由事件
某些对象参与与主可视化树的关系,该关系在概念上类似于在主视觉对象上覆盖。 这些对象不是将所有树元素连接到可视根的常规父子关系中的一部分。 这是适用于任何显示的 弹出窗口或 工具提示的情况。 如果要处理来自弹出窗口或工具提示的路由事件,请将处理程序放置在弹出窗口或工具提示中,而不是 Popup 或 ToolTip 元素本身的特定 UI 元素上。 不要依赖于针对 Popup 或 ToolTip 内容执行的任何组合内部的路由。 这是因为对于路由事件,其事件路由仅在主视觉树中有效。 弹出窗口或工具提示不被视为辅助 UI 元素的父级,并且永远不会收到路由事件,即使它尝试使用 Popup 默认背景之类的内容作为输入事件的捕获区域。
命中测试和输入事件
确定 UI 中元素在何处对鼠标、触摸和触笔输入可见,称为 命中测试。 对于触控操作以及特定于交互或操作的事件,元素必须在命中测试中可见才能成为事件源,并触发与操作关联的事件。 否则,操作会从该元素传递到可视化树中任何可与该输入交互的底层元素或父元素的。 有几个因素会影响命中测试,但可以通过检查给定元素 的 IsHitTestVisible 属性来确定给定元素是否可以触发输入事件。 仅当元素满足以下条件时,此属性才返回 true :
- 元素的 Visibility 属性值为 Visible。
- 元素的背景属性值或填充属性值不为null。 nullBrush 值会导致透明效果,并使对象在命中测试中不可见。 (若要使元素透明但也可以被命中检测,请使用 透明 画笔而不是 null。)
请注意,背景 和 填充 不是由 UIElement 定义的,而是由不同的派生类(如 Control 和 Shape)定义。 但是,无论哪个子类实现这些属性,用于前景和背景属性的画笔在命中测试和输入事件中的含义都是相同的。
- 如果该元素是控件,则其 IsEnabled 属性值必须为 true。
- 元素必须在布局中具有实际维度。 ActualHeight 和 ActualWidth 为 0 的元素不会触发输入事件。
某些控件具有用于命中测试的特殊规则。 例如, TextBlock 没有 Background 属性,但仍可在其维度的整个区域中进行命中测试。 Image 和 MediaElement 控件可以在其定义的矩形尺寸范围内进行命中测试,无论显示的媒体源文件中是否包含 alpha 通道等透明内容。 WebView 控件具有特殊的命中测试行为,因为输入可由托管的 HTML 处理并触发脚本事件。
大多数 面板 类和 边框 在其自己的背景中不可进行命中测试,但仍可以处理从其包含的元素路由的用户输入事件。
你可以确定哪些元素与用户输入事件位于同一位置,而不管这些元素是否可检测。 为此,请调用 FindElementsInHostCoordinates 方法。 顾名思义,此方法查找相对于指定主机元素的位置上的元素。 但是,应用的转换和布局更改可以调整元素的相对坐标系,从而影响在给定位置找到哪些元素。
指挥
少量 UI 元素支持 命令。 命令在其基础实现中使用与输入相关的路由事件,并通过调用单个命令处理程序启用对相关 UI 输入(特定指针作、特定快捷键)的处理。 如果命令可用于 UI 元素,请考虑使用其命令 API 而不是任何离散输入事件。 通常在定义数据视图模型的类的属性中使用绑定引用。 这些属性包含实现特定于语言的 ICommand 命令模式的命名命令。 有关详细信息,请参阅 ButtonBase.Command。
Windows 运行时中的自定义事件
为了定义自定义事件,如何添加事件,以及类设计的含义在很大程度上取决于所使用的编程语言。
- 对于 C# 和 Visual Basic,你正在定义 CLR 事件。 可以使用标准 .NET 事件模式,只要不使用自定义访问器(添加/删除)。 其他提示:
- 对于事件处理程序,最好使用 System.EventHandler<TEventArgs> ,因为它已内置转换为 Windows 运行时泛型事件委托 EventHandler<T>。
- 不要将事件数据类基于System.EventArgs,因为它无法转换为 Windows Runtime。 使用现有事件数据类或根本不使用基类。
- 如果使用自定义访问器,请参阅 Windows 运行时组件中的自定义事件和事件访问器。
- 如果不清楚标准 .NET 事件模式是什么,请参阅 定义自定义 Silverlight 类的事件。 这是针对 Microsoft Silverlight 编写的,但它仍然是标准 .NET 事件模式的代码和概念的良好总结。
- 有关 C++/CX,请参阅事件(C++/CX)。
- 即使在自己实现自定义事件时,也要使用命名引用。 不要将 lambda 用于自定义事件,它可以创建循环引用。
不能为 Windows 运行时声明自定义路由事件;路由事件仅限于来自 Windows 运行时的集。
定义自定义事件通常作为定义自定义控件的一部分完成。 具有属性更改回调的依赖属性也是一种常见模式,还定义了依赖属性回调在一些或所有情况下由依赖属性回调触发的自定义事件。 控件的使用者不能访问你定义的属性更改回调,但提供一个通知事件是一个不错的替代方案。 有关详细信息,请参阅 自定义依赖项属性。