using System.Windows.Controls.Primitives; using Melskin.Extensions; namespace Melskin.Assists; /// /// InputAssist 类提供了一系列附加属性,用于增强 WPF 应用程序中输入控件的功能。这些功能包括设置占位符文本、占位符颜色、前缀和后缀内容等,支持 TextBoxBase、PasswordBox 和 ComboBox 控件。 /// public static class InputAssist { #region Attached Properties /// /// 用于设置或获取控件的占位符文本。 /// public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.RegisterAttached("PlaceholderText", typeof(string), typeof(InputAssist), new PropertyMetadata(string.Empty)); /// /// 获取输入控件的占位符文本。 /// [AttachedPropertyBrowsableForType(typeof(TextBoxBase))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] public static string GetPlaceholderText(DependencyObject obj) { return (string)obj.GetValue(PlaceholderTextProperty); } /// /// 设置输入控件的占位符。 /// /// 要设置占位符的依赖对象。 /// 占位符的文本值。 public static void SetPlaceholderText(DependencyObject obj, string value) { obj.SetValue(PlaceholderTextProperty, value); } /// /// 用于设置或获取控件的前缀内容。 /// public static readonly DependencyProperty PrefixProperty = DependencyProperty.RegisterAttached("Prefix", typeof(object), typeof(InputAssist), new PropertyMetadata(null)); /// /// 获取与指定依赖对象关联的前缀。 /// /// 要获取前缀的依赖对象。 /// 与指定依赖对象关联的前缀对象。 [AttachedPropertyBrowsableForType(typeof(TextBox))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] public static object GetPrefix(DependencyObject obj) { return obj.GetValue(PrefixProperty); } /// /// 设置输入控件的前缀。 /// /// 要设置前缀的依赖对象。 /// 要设置为前缀的对象。 public static void SetPrefix(DependencyObject obj, object value) { obj.SetValue(PrefixProperty, value); } // 这是一个只读的依赖属性,用于向XAML报告PasswordBox是否有文本。 // 只有这个类内部可以通过Key来修改它的值,保证了状态的可靠性。 private static readonly DependencyPropertyKey HasTextPropertyKey = DependencyProperty.RegisterAttachedReadOnly( "HasPassword", typeof(bool), typeof(InputAssist), new PropertyMetadata(false)); /// /// 用于获取或设置是否显示密码框内有内容的指示器。 /// public static readonly DependencyProperty HasPasswordProperty = HasTextPropertyKey.DependencyProperty; /// /// 获取指定控件是否启用了密码模式。 /// /// 要检查的DependencyObject对象。 /// 如果控件启用了密码模式,则返回true;否则返回false。 [AttachedPropertyBrowsableForType(typeof(PasswordBox))] public static bool GetHasPassword(DependencyObject obj) { return (bool)obj.GetValue(HasPasswordProperty); } // 核心更新逻辑 private static void UpdateHasPassword(PasswordBox passwordBox) { // 根据密码长度是否大于0,来设置HasText属性的值 passwordBox.SetValue(HasTextPropertyKey, passwordBox.Password.Length > 0); } /// /// 用于设置或获取附着到控件上的后缀内容。 /// public static readonly DependencyProperty SuffixProperty = DependencyProperty.RegisterAttached("Suffix", typeof(object), typeof(InputAssist), new PropertyMetadata(null)); /// /// 获取附加到指定控件的后缀内容。 /// /// 要获取后缀内容的DependencyObject对象。 /// 返回控件的后缀内容,如果未设置则为null。 [AttachedPropertyBrowsableForType(typeof(TextBox))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] public static object GetSuffix(DependencyObject obj) { return obj.GetValue(SuffixProperty); } /// /// 设置输入控件的后缀。 /// /// 要设置后缀的依赖对象。 /// 要设置为后缀的对象。 public static void SetSuffix(DependencyObject obj, object value) { obj.SetValue(SuffixProperty, value); } #region PasswordBox /// /// 用于设置或获取控件的密码文本。 /// public static readonly DependencyProperty PasswordProperty = DependencyProperty.RegisterAttached("Password", typeof(string), typeof(InputAssist), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPasswordChanged)); private static void OnPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is PasswordBox passwordBox) { // 解除再绑定,防止重复订阅 passwordBox.PasswordChanged -= PasswordBox_PasswordChanged; var newVal = (string)e.NewValue ?? string.Empty; if (passwordBox.Password != newVal) { passwordBox.Password = newVal; } UpdateHasPassword(passwordBox); // 确保监听用户输入 passwordBox.PasswordChanged += PasswordBox_PasswordChanged; } } private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e) { if (sender is not PasswordBox box) return; var attached = GetPassword(box); var actual = box.Password; // 当用户输入变化时同步回附加属性 if (attached != actual) { // 这里调用 SetPassword 会再次触发 OnPasswordChanged,但因值相同不会再写入造成循环 SetPassword(box, actual); } UpdateHasPassword(box); } /// /// 获取输入控件的密码文本。 /// [AttachedPropertyBrowsableForType(typeof(PasswordBox))] public static string GetPassword(DependencyObject obj) { return (string)obj.GetValue(PasswordProperty); } /// /// 设置输入控件的密码文本。 /// public static void SetPassword(DependencyObject obj, string value) { obj.SetValue(PasswordProperty, value); } /// /// 切换可见密码的样板的作用 /// public static readonly DependencyProperty IsPasswordVisibleProperty = DependencyProperty.RegisterAttached("IsPasswordVisible", typeof(bool), typeof(InputAssist), new PropertyMetadata(false)); /// /// Gets whether the password currently held by PasswordBox is displayed in text. /// [AttachedPropertyBrowsableForType(typeof(PasswordBox))] public static bool GetIsPasswordVisible(DependencyObject obj) { return (bool)obj.GetValue(IsPasswordVisibleProperty); } /// /// 设置是否可以切换密码框的显示模式(显示/隐藏密码)。 /// /// 依赖对象,通常是PasswordBox。 /// 布尔值,表示是否启用显示/隐藏密码的功能。 public static void SetIsPasswordVisible(DependencyObject obj, bool value) { obj.SetValue(IsPasswordVisibleProperty, value); } #endregion /// /// 用于设置或获取控件是否可以被清空。 /// public static readonly DependencyProperty ClearEnabledProperty = DependencyProperty.RegisterAttached("ClearEnabled", typeof(bool), typeof(InputAssist), new PropertyMetadata(false, OnClearEnabledChanged)); /// /// 获取指定控件是否启用了清除功能。 /// /// 要获取属性的依赖对象。 /// 如果启用了清除功能,则返回 true;否则返回 false。 [AttachedPropertyBrowsableForType(typeof(TextBox))] [AttachedPropertyBrowsableForType(typeof(PasswordBox))] [AttachedPropertyBrowsableForType(typeof(ComboBox))] public static bool GetClearEnabled(DependencyObject obj) { return (bool)obj.GetValue(ClearEnabledProperty); } /// /// 设置输入控件是否可以被清空。 /// public static void SetClearEnabled(DependencyObject obj, bool value) { obj.SetValue(ClearEnabledProperty, value); } private static void OnClearEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not UIElement element) return; // 根据新值订阅或取消订阅鼠标左键释放事件 if ((bool)e.NewValue) { element.MouseLeftButtonUp += OnClear; } else { element.MouseLeftButtonUp -= OnClear; } } /// /// 用于设置或获取控件是否显示清除按钮。 /// public static readonly DependencyProperty ClearableProperty = DependencyProperty.RegisterAttached( "Clearable", typeof(bool), typeof(InputAssist), new PropertyMetadata(default(bool))); /// /// 设置输入控件是否可清除。 /// /// 要设置属性的依赖对象。 /// 布尔值,表示输入控件是否可以被用户清除其内容。 public static void SetClearable(DependencyObject d, bool value) { d.SetValue(ClearableProperty, value); } /// /// 获取指定依赖对象的可清除属性值。 /// /// 要获取属性值的依赖对象。 /// 返回一个布尔值,表示该控件是否可以被清除内容。 public static bool GetClearable(DependencyObject d) { return (bool)d.GetValue(ClearableProperty); } #endregion #region Private Methods /// /// 清除事件的处理程序 /// /// /// private static void OnClear(object sender, RoutedEventArgs e) { if (sender is not DependencyObject d) return; //查找父级控件中第一个是 TextBox、PasswordBox 或 ComboBox 的控件 var parent = d.GetAncestors().FirstOrDefault(a => a is TextBox or PasswordBox or ComboBox); if (parent != null && !GetClearable(parent)) return; switch (parent) { case TextBox textBox: textBox.Clear(); textBox.GetBindingExpression(TextBox.TextProperty)?.UpdateSource(); break; case PasswordBox passwordBox: passwordBox.Clear(); passwordBox.GetBindingExpression(PasswordProperty)?.UpdateSource(); break; case ComboBox comboBox: { if (comboBox.IsEditable) { comboBox.Text = string.Empty; comboBox.GetBindingExpression(ComboBox.TextProperty)?.UpdateSource(); } comboBox.SelectedItem = null; comboBox.GetBindingExpression(Selector.SelectedItemProperty)?.UpdateSource(); break; } } } #endregion }