WPF 如何使用验证和绑定创建自定义文本框

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/27973220/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 12:50:59  来源:igfitidea点击:

WPF How to create a Custom Textbox with validation and binding

c#wpfxamlbindingcustom-controls

提问by Rafael Ravena Vicente

I'm developing a custom text box for currency editing.
I've seen some ready to use ones, but they're complicated and/or not really usable, forcing you to bad practices (such as hard coding the name that's supposed to be used on the control).
So I've decided to do it myself, but I'm having trouble to work with the binding options, since the property assigned to the binding attribute must be a decimal, but the Text property of the TextBox control accepts strings.
The answer I thought was, maybe, override the access methods (getters and setters) to the Text property in the base class (TextBox), but it is not allowed.
My binding should be set to the value, that sets the text property of the TextBox formatting it as text (with the currency symbols and everything) on the go, but converting it back to a numeric datatype on the Get method.
This is what I've achieved so far:

我正在开发用于货币编辑的自定义文本框。
我见过一些现成的,但它们很复杂和/或不是真正可用,迫使您采取不良做法(例如硬编码应该在控件上使用的名称)。
所以我决定自己做,但是我在处理绑定选项时遇到了麻烦,因为分配给绑定属性的属性必须是小数,但 TextBox 控件的 Text 属性接受字符串。
我想的答案是,也许,将访问方法(getter 和 setter)覆盖到基类 (TextBox) 中的 Text 属性,但这是不允许的。
我的绑定应设置为值,该值将 TextBox 的文本属性设置为移动中的文本(带有货币符号和所有内容),但在 Get 方法中将其转换回数字数据类型。
这是我迄今为止取得的成就:

public class CurrencyTextBox : TextBox
    {
        private bool IsValidKey(Key key)
        {
            int k = (int)key;
            return ((k >= 34 && k <= 43) //digits 0 to 9
                || (k >= 74 && k <= 83) //numeric keypad 0 to 9
                || (k == 2) //back space
                || (k == 32) //delete
                );
        }
        private void Format()
        {
            //formatting decimal to currency text here
            //Done! no problems here
        }
        private void FormatBack()
        {
            //formatting currency text to decimal here
            //Done! no problems here
        }
        private void ValueChanged(object sender, TextChangedEventArgs e)
        {
            this.Format();
        }
        private void MouseClicked(object sender, MouseButtonEventArgs e)
        {
            this.Format();
            // Prevent changing the caret index
            this.CaretIndex = this.Text.Length;
            e.Handled = true;
        }
        private void MouseReleased(object sender, MouseButtonEventArgs e)
        {
            this.Format();
            // Prevent changing the caret index
            this.CaretIndex = this.Text.Length;
            e.Handled = true;
        }
        private void KeyPressed(object sender, KeyEventArgs e)
        {
            if (IsValidKey(e.Key))
                e.Handled = true;
            if (Keyboard.Modifiers != ModifierKeys.None)
                return;
            this.Format();
        }
        private void PastingEventHandler(object sender, DataObjectEventArgs e)
        {
            // Prevent copy/paste
            e.CancelCommand();
        }
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            // Disable copy/paste
            DataObject.AddCopyingHandler(this, PastingEventHandler);
            DataObject.AddPastingHandler(this, PastingEventHandler);
            this.CaretIndex = this.Text.Length;
            this.PreviewKeyUp += KeyPressed;
            this.PreviewMouseDown += MouseClicked;
            this.PreviewMouseUp += MouseReleased;
            this.TextChanged += ValueChanged;
            this.Format();
        }
    }

and this is the XAML:

这是 XAML:

<MyNamespace:CurrencyTextBox x:Name="TxbCurrency" Text="{Binding Path=DataContext.Element.Currency, ValidatesOnDataErrors=True}" />

So far so good! The binding from the decimal property to the TextBox text's is "right on". But how to get the decimal back from the text after it's editing is now the problem.
The binding from decimal to the .Text uses boxing to hide the ToString() method.
Question here:How can I overload the Parse() method from decimal in this case to use my FormatBack() method to get the decimal from the TextBox's Text?

到现在为止还挺好!从 decimal 属性到 TextBox 文本的绑定是“正确的”。但是现在的问题是如何在编辑后从文本中取回小数。
从十进制到 .Text 的绑定使用装箱来隐藏 ToString() 方法。
这里的问题:在这种情况下,如何从十进制重载 Parse() 方法以使用我的 FormatBack() 方法从 TextBox 的文本中获取小数?

采纳答案by safi

create new Dependency Propertylike this

Dependency Property像这样创建新的

public static readonly DependencyProperty ValueProperty = 
     DependencyProperty.Register(
         "Value", 
         typeof(decimal?),
         typeof(CurrencyTextBox),
         new FrameworkPropertyMetadata(
                     new decimal?(), 
                     FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
                     new PropertyChangedCallback(ValuePropertyChanged)));

private static void ValuePropertyChanged(
                         DependencyObject d,
                         DependencyPropertyChangedEventArgs e)
{
    CurrencyTextBox x = (CurrencyTextBox)d;
    x.Value = (decimal?)e.NewValue;
}

and then bind to this new property

然后绑定到这个新属性

回答by Rafael Ravena Vicente

Well, for future purposes, if anybody is stuck with the same trouble, here's the complete code for the currency text box. Feel free to use it, modify it, sell it (don't think it's valuable, thou), or play with it as much as you want!

好吧,为了将来的目的,如果有人遇到同样的问题,这里是货币文本框的完整代码。随意使用它、修改它、出售它(不要认为它很有价值,你),或者随心所欲地玩它!

/*
 * the necessary usings:
 * using System.Globalization;
 * using System.Windows;
 * using System.Windows.Controls;
 * using System.Windows.Input;
 * using System.Threading;
 * And don't forget to change the currency settings on the XAML
 * or in the defaults (on the contructor)
 * It's set by default to Brazilian Real (R$)
 */
public class CurrencyTextBox : TextBox
{
    public CurrencyTextBox()
    {
        CurrencySymbol = "R$ ";
        CurrencyDecimalPlaces = 2;
        DecimalSeparator = ",";
        ThousandSeparator = ".";
        Culture = "pt-BR";
    }
    public string CurrencySymbol { get; set; }
    private int CurrencyDecimalPlaces { get; set; }
    public string DecimalSeparator { get; set; }
    public string ThousandSeparator { get; set; }
    public string Culture { get; set; }
    private bool IsValidKey(int k)
    {
        return (k >= 34 && k <= 43) //digits 0 to 9
            || (k >= 74 && k <= 83) //numeric keypad 0 to 9
            || (k == 2) //back space
            || (k == 32) //delete
            ;
    }
    private string Format(string text)
    {
        string unformatedString = text == string.Empty ? "0,00" : text; //Initial state is always string.empty
        unformatedString = unformatedString.Replace(CurrencySymbol, ""); //Remove currency symbol from text
        unformatedString = unformatedString.Replace(DecimalSeparator, ""); //Remove separators (decimal)
        unformatedString = unformatedString.Replace(ThousandSeparator, ""); //Remove separators (thousands)
        decimal number = decimal.Parse(unformatedString) / (decimal)Math.Pow(10, CurrencyDecimalPlaces); //The value will have 'x' decimal places, so divide it by 10^x
        unformatedString = number.ToString("C", CultureInfo.CreateSpecificCulture(Culture));
        return unformatedString;
    }
    private decimal FormatBack(string text)
    {
        string unformatedString = text == string.Empty ? "0.00" : text;
        unformatedString = unformatedString.Replace(CurrencySymbol, ""); //Remove currency symbol from text
        unformatedString = unformatedString.Replace(ThousandSeparator, ""); //Remove separators (thousands);
        CultureInfo current = Thread.CurrentThread.CurrentUICulture; //Let's change the culture to avoid "Input string was in an incorrect format"
        Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(Culture);
        decimal returnValue = decimal.Parse(unformatedString);
        Thread.CurrentThread.CurrentUICulture = current; //And now change it back, cuz we don't own the world, right?
        return returnValue;
    }
    private void ValueChanged(object sender, TextChangedEventArgs e)
    {
        // Keep the caret at the end
        this.CaretIndex = this.Text.Length;
    }
    private void MouseClicked(object sender, MouseButtonEventArgs e)
    {
        // Prevent changing the caret index
        e.Handled = true;
        this.Focus();
    }
    private void MouseReleased(object sender, MouseButtonEventArgs e)
    {
        // Prevent changing the caret index
        e.Handled = true;
        this.Focus();
    }
    private void KeyReleased(object sender, KeyEventArgs e)
    {
        this.Text = Format(this.Text);
        this.Value = FormatBack(this.Text);
    }
    private void KeyPressed(object sender, KeyEventArgs e)
    {
        if (IsValidKey((int)e.Key))
            return;
        e.Handled = true;
        this.CaretIndex = this.Text.Length;
    }
    private void PastingEventHandler(object sender, DataObjectEventArgs e)
    {
        // Prevent/disable paste
        e.CancelCommand();
    }
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        DataObject.AddCopyingHandler(this, PastingEventHandler);
        DataObject.AddPastingHandler(this, PastingEventHandler);
        this.CaretIndex = this.Text.Length;
        this.KeyDown += KeyPressed;
        this.KeyUp += KeyReleased;
        this.PreviewMouseDown += MouseClicked;
        this.PreviewMouseUp += MouseReleased;
        this.TextChanged += ValueChanged;
        this.Text = Format(string.Empty);
    }
    public decimal? Value
    {
        get { return (decimal?)this.GetValue(ValueProperty); }
        set { this.SetValue(ValueProperty, value); }
    }
    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
        "Value",
        typeof(decimal?),
        typeof(CurrencyTextBox),
        new FrameworkPropertyMetadata(new decimal?(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(ValuePropertyChanged)));
    private static void ValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((CurrencyTextBox)d).Value = ((CurrencyTextBox)d).FormatBack(e.NewValue.ToString());
    }
}

and the xaml:

和xaml:

<myNamespace:CurrencyTextBox
    Value="{Binding Path=DataContext.MyDecimalProperty, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"
    CurrencySymbol="R$ "
    Culture="pt-BR"
    CurrencyDecimalPlaces="2"
    DecimalSeparator=","
    ThousandSeparator="." />

回答by safi

take look on this article i think it will help you. http://www.codeproject.com/Articles/15239/Validation-in-Windows-Presentation-Foundation

看看这篇文章,我认为它会对你有所帮助。 http://www.codeproject.com/Articles/15239/Validation-in-Windows-Presentation-Foundation

or you can put this

或者你可以把这个

private static bool IsTextAllowed(string text)
{
    Regex regex = new Regex("[^0-9.-]+"); //regex that matches disallowed text
    return !regex.IsMatch(text);
}

and in PreviewTextInputevent put this

并且在PreviewTextInput事件中把这个

e.Handled = !IsTextAllowed(e.Text);

e.Handled = !IsTextAllowed(e.Text);

回答by Robin Bennett

I don't think this is actually possible, except for the simple case of a box that only allows digits. Ideally you'd like a box that can only contain a valid entry, but decimals contain some characters (like '-' and '.') that are not valid on their own. The user can't start by typing a '-' without putting the box into an invalid state.

我认为这实际上是不可能的,除了只允许数字的框的简单情况。理想情况下,您需要一个只能包含有效条目的框,但小数包含一些本身无效的字符(如“-”和“.”)。用户不能通过键入“-”开始而不将框置于无效状态。

Similarly they could enter '1.', and then delete the 1 and leave the box in an indeterminate state. Sure, it causes a validation error and a red border, but your view model still thinks the value is 1, and isn't aware of the problem.

类似地,他们可以输入“1.”,然后删除 1 并使框处于不确定状态。当然,它会导致验证错误和红色边框,但您的视图模型仍然认为该值为 1,并且不知道问题所在。

For positive integers, you can only allow digits and automatically insert a zero when blank (although that's a little unfriendly)

对于正整数,您只能允许数字并在空白时自动插入零(虽然这有点不友好)

For decimals and negative integers, I think the best you can do is to constrain the keys a user can type, but you still need to wrap your number property in a string and validate it - either when the OK button is pressed, or ideally implement INotifyDataError to display the error and disable the OK button.

对于小数和负整数,我认为你能做的最好的事情是限制用户可以输入的键,但你仍然需要将你的数字属性包装在一个字符串中并验证它 - 无论是在按下 OK 按钮时,还是在理想情况下实现INotifyDataError 显示错误并禁用确定按钮。