TextBoxUserInputBehavior.cs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. using Microsoft.Xaml.Behaviors;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. using System.Windows;
  8. using System.Windows.Controls;
  9. using System.Windows.Input;
  10. namespace TeamAAS_VP.Behaviors
  11. {
  12. /// <summary>
  13. /// Behavior:在 TextBox 上检测“用户输入”并通过绑定的命令回调旧/新文本值。
  14. /// 仅在检测到明确的用户交互(键入、删除、粘贴、输入法确认等)时触发命令,避免程序性修改触发。
  15. /// </summary>
  16. public class TextBoxUserInputBehavior : Behavior<TextBox>
  17. {
  18. #region 依赖属性
  19. /// <summary>
  20. /// 绑定的命令,当检测到用户输入且命令可执行时调用。
  21. /// 命令接收类型为 <see cref="TextChangedEventArgs"/>(自定义)。
  22. /// </summary>
  23. public static readonly DependencyProperty UserInputCommandProperty =
  24. DependencyProperty.Register(
  25. nameof(UserInputCommand),
  26. typeof(ICommand),
  27. typeof(TextBoxUserInputBehavior));
  28. /// <summary>
  29. /// 要执行的命令(由视图模型绑定)。
  30. /// </summary>
  31. public ICommand UserInputCommand
  32. {
  33. get => (ICommand)GetValue(UserInputCommandProperty);
  34. set => SetValue(UserInputCommandProperty, value);
  35. }
  36. /// <summary>
  37. /// 可选的外部命令参数,将透传到自定义事件参数中。
  38. /// </summary>
  39. public static readonly DependencyProperty CommandParameterProperty =
  40. DependencyProperty.Register(
  41. nameof(CommandParameter),
  42. typeof(object),
  43. typeof(TextBoxUserInputBehavior));
  44. /// <summary>
  45. /// 绑定的命令参数(用于在触发命令时携带额外上下文)。
  46. /// </summary>
  47. public object CommandParameter
  48. {
  49. get => GetValue(CommandParameterProperty);
  50. set => SetValue(CommandParameterProperty, value);
  51. }
  52. #endregion
  53. #region 私有字段
  54. // 是否由用户触发的输入(用于在 TextChanged 中判断)
  55. private bool _isUserInput = false;
  56. // 记录变更前的文本值,用于在触发时传递旧值
  57. private string _previousText;
  58. #endregion
  59. #region 附加/分离
  60. /// <summary>
  61. /// Behavior 附加到 TextBox 时订阅必要的事件。
  62. /// 订阅 PreviewTextInput、PreviewKeyDown、TextChanged、LostFocus 以检测不同的用户交互场景。
  63. /// </summary>
  64. protected override void OnAttached()
  65. {
  66. base.OnAttached();
  67. AssociatedObject.PreviewTextInput += OnPreviewTextInput;
  68. AssociatedObject.PreviewKeyDown += OnPreviewKeyDown;
  69. AssociatedObject.TextChanged += OnTextChanged;
  70. AssociatedObject.LostFocus += OnLostFocus;
  71. }
  72. /// <summary>
  73. /// Behavior 分离时取消订阅事件,防止内存泄漏。
  74. /// </summary>
  75. protected override void OnDetaching()
  76. {
  77. base.OnDetaching();
  78. AssociatedObject.PreviewTextInput -= OnPreviewTextInput;
  79. AssociatedObject.PreviewKeyDown -= OnPreviewKeyDown;
  80. AssociatedObject.TextChanged -= OnTextChanged;
  81. AssociatedObject.LostFocus -= OnLostFocus;
  82. }
  83. #endregion
  84. #region 事件处理
  85. /// <summary>
  86. /// 处理预输入文本事件(用户通过键盘输入字符)。
  87. /// 标记为用户输入并保存变更前的文本。
  88. /// </summary>
  89. private void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
  90. {
  91. // 用户正在键入文本,记录标志并缓存旧值
  92. _isUserInput = true;
  93. _previousText = AssociatedObject.Text;
  94. }
  95. /// <summary>
  96. /// 处理按键事件(用于检测删除、退格、空格及粘贴快捷键 Ctrl+V)。
  97. /// 对这些键也将视为用户输入。
  98. /// </summary>
  99. private void OnPreviewKeyDown(object sender, KeyEventArgs e)
  100. {
  101. // 检测删除、退格、空格等按键,这些也属于用户输入操作
  102. if (e.Key == Key.Back || e.Key == Key.Delete || e.Key == Key.Space)
  103. {
  104. _isUserInput = true;
  105. _previousText = AssociatedObject.Text;
  106. }
  107. // 处理粘贴操作 (Ctrl+V),将其视为用户输入
  108. if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
  109. {
  110. _isUserInput = true;
  111. _previousText = AssociatedObject.Text;
  112. }
  113. }
  114. /// <summary>
  115. /// TextChanged 事件处理器:当文本发生变化且标记为用户输入时,构造参数并执行绑定命令。
  116. /// 之后清除用户输入标记以避免重复触发。
  117. /// </summary>
  118. private void OnTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
  119. {
  120. if (_isUserInput)
  121. {
  122. // 清除标记,避免多次触发
  123. _isUserInput = false;
  124. // 如果命令可执行,构造包含旧值/新值的参数并执行
  125. if (UserInputCommand?.CanExecute(CommandParameter) == true)
  126. {
  127. var parameter = new TextChangedEventArgs(
  128. CommandParameter,
  129. _previousText,
  130. AssociatedObject.Text);
  131. UserInputCommand.Execute(parameter);
  132. }
  133. }
  134. }
  135. /// <summary>
  136. /// LostFocus 事件处理器:处理通过输入法(IME)输入时,确认输入后触发命令。
  137. /// 当用户在输入法中输入完成并失去焦点时也应视为用户输入。
  138. /// </summary>
  139. private void OnLostFocus(object sender, RoutedEventArgs e)
  140. {
  141. // 如果之前有标记为用户输入(例如输入法未触发 TextChanged),在失焦时触发一次
  142. if (_isUserInput)
  143. {
  144. _isUserInput = false;
  145. if (UserInputCommand?.CanExecute(CommandParameter) == true)
  146. {
  147. var parameter = new TextChangedEventArgs(
  148. CommandParameter,
  149. _previousText,
  150. AssociatedObject.Text);
  151. UserInputCommand.Execute(parameter);
  152. }
  153. }
  154. }
  155. #endregion
  156. }
  157. /// <summary>
  158. /// 自定义事件参数:用于封装命令参数、文本变更的旧值与新值。
  159. /// 该类是不可变的,仅用于传递数据到命令执行逻辑中。
  160. /// </summary>
  161. public class TextChangedEventArgs
  162. {
  163. /// <summary>
  164. /// 外部传入的命令参数(如果有)。
  165. /// </summary>
  166. public object CommandParameter { get; }
  167. /// <summary>
  168. /// 变更前的文本值。
  169. /// </summary>
  170. public string OldValue { get; }
  171. /// <summary>
  172. /// 变更后的文本值。
  173. /// </summary>
  174. public string NewValue { get; }
  175. /// <summary>
  176. /// 构造函数:创建包含命令上下文和文本新旧值的参数对象。
  177. /// </summary>
  178. public TextChangedEventArgs(object commandParameter, string oldValue, string newValue)
  179. {
  180. CommandParameter = commandParameter;
  181. OldValue = oldValue;
  182. NewValue = newValue;
  183. }
  184. }
  185. }