示例工程【免费下载链接】WPF-SamplesRepository for WPF related samples项目地址https://gitcode.com/gh_mirrors/wp/WPF-Samples点击查看免费下载导读本篇技术指南以 WPF-Samples 仓库中Input and Commands/ICommandSourceImplementation示例为核心讲解如何通过继承 WPF 控件并实现ICommandSource接口将普通的Slider改造为可以触发命令的自定义控件。读完本文你将掌握ICommandSource三大核心成员Command、CommandTarget、CommandParameter的依赖属性化设计、CanExecuteChanged事件订阅与状态联动、以及自定义命令源在 XAML 中的完整接入方法并能独立实现其他任意自定义控件的命令源化改造。示例概览示例在做什么ICommandSourceImplementation示例的核心目标是演示如何在自定义控件上实现ICommandSource接口。用原文档README.md的原始描述来说This sample demonstrates how to create a custom ICommandSource by implementing ICommand on a custom control.即通过在自定义控件上实现ICommand相关接口创建一个自定义命令源。示例中的自定义控件继承自Slider并实现了ICommandSource——控件名为CommandSlider源码见 commandslider.cs。运行效果上窗口内有一个字号滑杆CommandSlider拖动滑杆即可触发FontUpdateCommand命令实时改变右侧TextBox的字体大小同时界面还提供了“ / -”按钮与右键菜单、普通按钮等多个命令源全部指向同一个FontUpdateCommand演示了多命令源共用一条命令的典型场景。一、ICommandSource接口与三个核心成员ICommandSource是 WPF 命令体系中的关键接口定义在System.Windows.Input命名空间要求实现类提供三个成员成员类型含义CommandICommand命令源要执行的命令对象CommandTargetIInputElement命令要作用的“目标元素”路由命令会沿着逻辑树/可视化树从目标向上查找CommandBindingCommandParameterobject传递给命令执行与可执行性判断的用户数据WPF 内置的Button、MenuItem等控件都是ICommandSource的典型实现。本示例的价值在于展示当内置控件不满足需求时如何在自己的自定义控件上完整复刻这套命令源机制。在 commandslider.cs 中类声明即为public class CommandSlider : Slider, ICommandSource二、将接口成员实现为依赖属性依赖属性化ICommandSource的三个接口成员本身只是普通属性但示例代码刻意将三者全部注册为依赖属性Dependency Property目的是让它们支持 XAML 绑定与样式触发。这一点在原文档中没有展开却是本示例最值得学习的工程细节。三个依赖属性的注册代码如下见 commandslider.cs// Make Command a dependency property so it can use databinding. public static readonly DependencyProperty CommandProperty DependencyProperty.Register( Command, typeof(ICommand), typeof(CommandSlider), new PropertyMetadata(null, CommandChanged)); // Make CommandTarget a dependency property so it can use databinding. public static readonly DependencyProperty CommandTargetProperty DependencyProperty.Register( CommandTarget, typeof(IInputElement), typeof(CommandSlider), new PropertyMetadata((IInputElement)null)); // Make CommandParameter a dependency property so it can use databinding. public static readonly DependencyProperty CommandParameterProperty DependencyProperty.Register( CommandParameter, typeof(object), typeof(CommandSlider), new PropertyMetadata((object)null));要点解析Command属性带有一个属性变更回调CommandChanged用于在命令对象更换时重新挂接/卸载事件处理器CommandTarget与CommandParameter仅注册为依赖属性未附加回调——它们的取值会在命令执行时按需读取对应的 CLR 包装属性通过GetValue/SetValue读写依赖属性见 commandslider.cs。正因为在 XAML 中CommandSlider的Command、CommandParameter都是依赖属性示例的 MainWindow.xaml 才能这样直接书写绑定custom:CommandSlider x:NameFontSlider BackgroundAliceBlue Minimum0 Maximum60 Value12 TickFrequency5 TickPlacementBottomRight LargeChange5 SmallChange5 AutoToolTipPlacementBottomRight AutoToolTipPrecision0 Command{x:Static custom:MainWindow.FontUpdateCommand} CommandTarget{Binding ElementNametxtBoxTarget} CommandParameter{Binding ElementNameFontSlider, PathValue, Converter{StaticResource DoubleConverterResource}} FocusableFalse /其中Command通过x:Static指向静态路由命令CommandParameter通过Binding把滑杆的Valuedouble经FontDoubleValueConverter转换成 int 后作为参数传入。三、命令的挂接与卸载HookUpCommand 机制依赖属性Command的值发生变化时CommandChanged回调被触发示例通过HookUpCommand完成新旧命令的事件挂接与清理见 commandslider.cs// Command dependency property change callback. private static void CommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var cs (CommandSlider)d; cs.HookUpCommand((ICommand)e.OldValue, (ICommand)e.NewValue); } // Add a new command to the Command Property. private void HookUpCommand(ICommand oldCommand, ICommand newCommand) { if (oldCommand ! null) { RemoveCommand(oldCommand, newCommand); } AddCommand(oldCommand, newCommand); } // Remove an old command from the Command Property. private void RemoveCommand(ICommand oldCommand, ICommand newCommand) { EventHandler handler CanExecuteChanged; oldCommand.CanExecuteChanged - handler; } // Add the command. private void AddCommand(ICommand oldCommand, ICommand newCommand) { var handler new EventHandler(CanExecuteChanged); _canExecuteChangedHandler handler; if (newCommand ! null) { newCommand.CanExecuteChanged _canExecuteChangedHandler; } }这里有一个很容易被忽视的坑示例代码专门用注释强调// Keep a copy of the handler so it doesnt get garbage collected. private static EventHandler _canExecuteChangedHandler;关键点CanExecuteChanged的事件处理器必须被静态字段_canExecuteChangedHandler持有引用。原因是 WPF 内部在实现RoutedCommand时会对CanExecuteChanged的订阅者做弱引用缓存weak reference若没有强引用保存处理器它可能被垃圾回收导致命令源收不到命令状态变更通知。这是实现自定义命令源时最容易踩的坑也是本示例源码对“接口实现细节”的重要补充。四、命令可用性联动CanExecuteChanged 与 IsEnabled实现ICommandSource后命令源应具备与Button相同的行为当命令不可执行时自动禁用自己。示例在CanExecuteChanged事件处理器中完成这一逻辑见 commandslider.csprivate void CanExecuteChanged(object sender, EventArgs e) { if (Command ! null) { var command Command as RoutedCommand; // If a RoutedCommand. IsEnabled command?.CanExecute(CommandParameter, CommandTarget) ?? Command.CanExecute(CommandParameter); } }这段代码展示了ICommand的两个分支调用方式若命令是RoutedCommand路由命令调用CanExecute(parameter, target)——路由命令的可执行性判断会把CommandTarget作为路由起点沿元素树查找CommandBinding上的CanExecute处理器否则普通ICommand直接调用CanExecute(parameter)。返回值直接写入IsEnabled从而让滑杆在命令不可执行时变为灰色禁用状态。五、命令触发重写 OnValueChanged 在值变化时执行命令作为命令源CommandSlider需要在“用户操作发生”时执行命令。对Slider而言这个时机就是滑杆值变化。示例重写了Slider.OnValueChanged见 commandslider.cs// If Command is defined, moving the slider will invoke the command; // Otherwise, the slider will behave normally. protected override void OnValueChanged(double oldValue, double newValue) { base.OnValueChanged(oldValue, newValue); if (Command ! null) { var command Command as RoutedCommand; if (command ! null) { command.Execute(CommandParameter, CommandTarget); } else { Command.Execute(CommandParameter); } } }同样分两类执行RoutedCommand走Execute(parameter, target)命令沿CommandTarget向上路由由沿途的CommandBinding处理普通ICommand走Execute(parameter)。值得注意的优雅设计是当Command为 null 时控件退化为普通Slider行为完全正常。也就是说“命令源化”是叠加在原有控件能力之上的增强而非破坏性改造——这也正是自定义命令源设计的一个通用原则。六、完整的命令回路MainWindow 中的命令定义与处理CommandSlider只是命令源真正的命令处理逻辑位于窗口代码。MainWindow定义了一个静态路由命令见 MainWindow.cspublic static RoutedCommand FontUpdateCommand new RoutedCommand();命令的Executed与CanExecute处理器通过TextBox.CommandBindings挂接到命令目标上见 MainWindow.xamlTextBox NametxtBoxTarget Height100 Width275 Margin3 TextBox.CommandBindings CommandBinding Command{x:Static custom:MainWindow.FontUpdateCommand} ExecutedSliderUpdateExecuted CanExecuteSliderUpdateCanExecute / /TextBox.CommandBindings ... /TextBox对应的两个处理器见 MainWindow.cs// The ExecutedRoutedEvent Handler private void SliderUpdateExecuted(object sender, ExecutedRoutedEventArgs e) { var source sender as TextBox; if (source ! null) { if (e.Parameter ! null) { try { if ((int)e.Parameter 0 (int)e.Parameter 60) { source.FontSize (int)e.Parameter; } } catch { MessageBox.Show(in Command \n Parameter: e.Parameter); } } } } // The CanExecuteRoutedEvent Handler private void SliderUpdateCanExecute(object sender, CanExecuteRoutedEventArgs e) { var source sender as TextBox; if (source ! null) { e.CanExecute !source.IsReadOnly; } }处理逻辑SliderUpdateExecuted把命令参数int 类型的字号设置到TextBox.FontSize并对参数做 0~60 的合法性校验SliderUpdateCanExecute依据目标TextBox的IsReadOnly状态决定命令是否可执行——勾选窗口中的 Read Only 复选框后命令将不可执行所有命令源随之禁用。复选框事件处理器通过CommandManager.InvalidateRequerySuggested()强制命令系统重新查询见 MainWindow.csprivate void OnReadOnlyChecked(object sender, RoutedEventArgs e) { if (txtBoxTarget ! null) { txtBoxTarget.IsReadOnly true; CommandManager.InvalidateRequerySuggested(); } }为什么必须调用InvalidateRequerySuggested正常情况下CommandManager在输入焦点变化、鼠标按键等交互发生时自动触发RequerySuggested事件进而让RoutedCommand引发CanExecuteChanged命令源随即重新查询可执行性。但当条件变化“不伴随任何输入事件”时例如这里纯粹由复选框状态改变IsReadOnly命令系统无法感知就需要手动调用InvalidateRequerySuggested()强制触发重新查询。这一机制在仓库的姊妹示例Input and Commands/CommandSourceControlUsingSystemTimer与Input and Commands/CommandSourceControlWithDispatcherTimer中也有印证——这两个示例分别用System.Timers.Timer和DispatcherTimer周期性地调用CommandManager.InvalidateRequerySuggested()从而让命令源每秒重新评估一次可执行性。七、多命令源协同与值转换器示例窗口展示了多个命令源共同驱动同一个命令的经典布局CommandSliderFontSlider拖动滑杆触发FontUpdateCommand“- / ”按钮分别绑定内置的Slider.DecreaseSmall/Slider.IncreaseSmall路由命令见 MainWindow.xamlCommandTarget指向FontSlider实现“按一下微调 5 个单位”此时FontSlider变成了按钮命令的命令目标右键菜单MenuItem在TextBox的ContextMenu中通过CommandParameter绑定ListBox选中项的文本10/20/30触发字体更新见 MainWindow.xaml独立按钮 输入框从txtValue文本框取值作为CommandParameter执行FontUpdateCommand见 MainWindow.xaml。这里还涉及两个值转换器详见 FontDoubleValueConverter.cs 与 FontStringValueConverter.csFontDoubleValueConverter把滑杆的double值转为int[ValueConversion(typeof(double), typeof(int))]FontStringValueConverter把菜单/输入框的字符串解析为int解析失败返回null。两个转换器的ConvertBack均返回null即只做单向转换。另外为了让“- / ”按钮在滑杆禁用时也同步禁用窗口为这两个内置命令挂了PreviewCanExecute级别的CommandBinding见 MainWindow.xaml处理函数IncreaseDecreaseCanExecute判断e.Source是否为启用的Slider见 MainWindow.csprivate void IncreaseDecreaseCanExecute(object sender, CanExecuteRoutedEventArgs e) { var target e.Source as Slider; if (target ! null) { if (target.IsEnabled) { e.CanExecute true; e.Handled true; } else { e.CanExecute false; e.Handled true; } } }设置e.Handled true是为了短路路由避免命令继续向上传播到其他CommandBinding。八、构建与运行工程结构本示例是一个标准的 WPF 可执行程序WinExe工程文件 ICommandSourceImplementation.csproj 关键配置TargetFramework为net10.0-windowsUseWpf为trueApp.xaml通过StartupUriMainWindow.xaml指定启动窗口见 App.xaml编译项明确包含CommandSlider.cs、两个转换器与MainWindow、App。部署与运行步骤原文档README.md给出的标准操作流程为部署在 Visual Studio 中选择Build Deploy Solution调试运行按F5或选择Debug Start Debugging无调试运行按CtrlF5或选择Debug Start Without Debugging。运行后拖动字号滑杆即可看到右侧TextBox字体大小随之变化勾选 Read Only 复选框后所有指向FontUpdateCommand的命令源滑杆、按钮、菜单项都会因为CanExecute变为 false 而统一禁用——这正是命令源机制“一处判断、处处生效”的直观体验。结语从示例提炼自定义命令源的实现范式通过阅读ICommandSourceImplementation示例的完整源码可以提炼出实现自定义ICommandSource的四步范式声明让自定义控件继承现有控件并实现ICommandSource接口依赖属性化将Command、CommandTarget、CommandParameter注册为依赖属性Command附带变更回调事件管理在CommandChanged回调中卸载旧命令、挂接新命令的CanExecuteChanged并用静态字段保强引用防 GC处理器内调用CanExecute并同步IsEnabled触发执行在控件对应的用户操作时机对 Slider 而言是OnValueChanged调用Execute并区分RoutedCommand带CommandTarget与普通ICommand仅带CommandParameter两种执行路径。这套范式不仅适用于Slider也适用于任何需要“由用户操作触发命令”的自定义控件是 WPF 命令系统扩展能力的标准做法。赞分享示例工程【免费下载链接】WPF-SamplesRepository for WPF related samples项目地址https://gitcode.com/gh_mirrors/wp/WPF-Samples点击查看免费下载相关推荐WPF 自定义 RoutedCommand 实战从代码创建、CommandBinding 绑定到 XAML 命令触发WPF-Samples 示例详解WPF 自定义 RoutedCommand 实战从代码创建、CommandBinding 绑定到 XAML 命令触发WPF Samples 示例详解 导读示例工程WPF FocusVisualStyle 实战指南用 FocusVisualStyle 为控件定制键盘焦点外观附 WPF-Samples 源码解析WPF FocusVisualStyle 实战指南用 FocusVisualStyle 为控件定制键盘焦点外观附 WPF Samples 源码解析 导读示例工程WPF 资源合并实战基于 WPF-Samples Merged ResourceDictionary 示例理解 ResourceDictionary.MergedDictionariesWPF 资源合并实战基于 WPF Samples Merged ResourceDictionary 示例理解 ResourceDictionary.Merg示例工程上一篇QuantDinger 策略生命周期实战从想法到实盘的完整工作流指南下一篇OpenZiti v0.33 发布解析SDK Terminator 稳定性强化与新诊断工具链创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?