65.9K
CodeProject 正在变化。 阅读更多。
Home

实现数字值到字符串(如 TextBox.Text)的双向绑定

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2010 年 4 月 26 日

CPOL

11分钟阅读

viewsIcon

52114

downloadIcon

806

提供了一个类, 可以方便地将普通 TextBox 绑定到数字属性

DsbDemo.png

引言

本文提出了一种解决方案,用于解决将 `TextBox` 控件绑定到数字(`decimal`、`float`、`int` 等)值的一般性问题。由于假定的上下文是业务编程,本文将 `decimal` 视为最通用的数字类型,尽管 `float` 和 `double` 可以表示更广泛的值范围。

背景

业务软件的开发者在接受包含小数点的用户输入的数字值时会遇到挑战,例如货币值和各种测量值(可能最常见的是重量)。您可能会觉得有必要购买专门的控件来解决相关问题,并且该选项可能效果很好;然而,本文旨在展示如何在处理 .NET Framework 提供的“纯粹、简单”的 `TextBox` 控件的同时,充分解决这个问题。尽管本文提出的解决方案以及配套代码是为了配合 Silverlight 开发的,但由于 WPF、ASP.NET 和 Windows Forms 的 TextBox 控件工作方式非常相似,因此很容易将此解决方案用于 .NET Framework 的任何 `TextBox` 控件,只需进行最少的(甚至无需)修改。

在显示数字数据以及接受用户输入的 `string` 时都需要进行转换,以便稍后将数据存储为某种数字类型的值和/或进行计算。

与格式化数字数据以供显示相关的问题相对简单。它们涉及

  • 以特定的标准或自定义数字格式显示数据,例如“四舍五入到两位小数,小数点左侧不分组”。
  • 考虑用户的文化。例如,美国的用户的期望是使用 '.' 字符表示小数点,而荷兰用户的期望是使用 ',' 表示小数点。(允许当前线程的文化来处理此问题是默认方法,但它并不总是足够或最合适的方法。)

处理上述问题并不复杂。如下所示的代码可以完成任务

myTextBox.Text = myDecimalValue.ToString("N2", userCulture);

但是,用户就是用户,接受和转换数字输入可能会变得有点麻烦。

  • :如果用户按其文化习惯插入千位分隔符对数字进行分组怎么办?
  • :如果用户输入的精度小于或大于预期怎么办?
  • :如果用户输入的字符串是“垃圾”字符串,无法成功转换为数字值怎么办?
  • :是否应采取措施来处理输入货币符号后跟货币值或百分号前跟百分比值的用户?

上述任何问题似乎都不难回答(尽管答案可能有所不同),并且实现所需的功能也不算太难。以下是随本文附带的可下载代码中实现的答案

  • :如果用户按其文化习惯插入千位分隔符对数字进行分组怎么办?
    :这是可以的。解析输入时会忽略千位分隔符。
  • :如果用户输入的精度小于或大于预期怎么办?
    :如果太少则用零(多个)填充;如果太多则截断。
  • :如果用户输入的字符串是“垃圾”`string`,无法成功转换为数字值怎么办?
    :解析无效输入的結果是零。
  • :是否应采取措施来处理输入货币符号后跟货币值或百分号前跟百分比值的用户?
    答:检查是否存在此类符号并将其删除;然后修剪剩余部分,以防货币符号后跟空格或百分号前跟空格。

考虑到上述所有问题,并考虑到可能需要编写大量临时代码来处理不同的情况,因此很明显,提供一个封装的解决方案——一个能够全面可靠地解决上述问题(甚至可能还有其他一些问题)的类——是很有价值的。

开发一个执行标准转换集并在绑定场景中充当中间者的类的第一个尝试自然会是一个转换器类(实现 `IValueConverter`)。但是,您可能会发现,希望类的方法(`Convert` 和 `ConvertBack`,或其功能等价物)能够访问其返回值将被分配的数字或 `string` 对象。这通常不是转换器的功能,并且“嫁接”进去可能有点困难且笨拙。

本文介绍的 `DecimalStringBinder` 类在技术上并非转换器类,因为它不实现 `IValueConverter`。因此,`DecimalStringBinder` 对象不能在 XAML 绑定语句中作为转换器引用。相反,`DecimalStringBinder` 对象可以通过 XAML 绑定语句直接绑定到控件。它们可以舒适自然地融入 MVVM 结构化解决方案的 `ViewModel` 层,如下所示。或者,`DecimalStringBinder` 对象可以在表示层代码隐藏文件中声明和管理。只有在没有任何混淆的情况下,我们才会暗示该类实现了 `IValueConverter`,此时可以 *松散地* 将 `DecimalStringBinder` 称为一种“转换器类”。

// View                       ViewModel                         Model [data contract]
// TextBox.Text <- binding -> DecimalStringBinder <- binding -> numeric property

Using the Code

在本节中,我将概述 `DecimalStringBinder`。请参阅“趣味点”部分,了解如何使用该类完成特定目标和解决特定问题的具体示例。

构造函数

提供了不少于七个构造函数重载,允许以多种方式初始化对象。当对象将参与 UI 绑定时,最通用的重载可能是这个

public DecimalStringBinder
    (INotifyPropertyChanged decimalPropertyParentObject, PropertyInfo decimalProperty)

属性

`DecimalStringBinder` 对象执行的大部分工作是通过属性完成的,这些属性由一些 `private` 方法支持。以下是 `public` 属性列表及其简要说明

CultureInfo UserCulture 获取或设置用户的文化 - 不一定与绑定数字属性的文化相同。
int DecimalPlaces 获取或设置用于解析和显示值的十进制位数。
Enums.NumericFormat OutputFormat 获取或设置 `RefreshStringValue` 方法使用的格式说明符。
string CustomOutputFormat 获取或设置 `RefreshStringValue` 方法使用的自定义格式字符串。如果同时指定了两者,则覆盖 `OutputFormat`……
NumberStyles NumberStyles 获取或设置用于解析输入 `StringValue` 的 `NumberStyles` 值。
string ZeroValue 获取或设置一个特殊值(如空字符串),用于表示零值。
string StringPrefix 获取或设置 `RefreshStringValue` 在格式化数字值时使用的可选前缀,并被识别为有效并从输入字符串(分配给 `StringValue`)中剥离。
string StringSuffix 获取或设置 `RefreshStringValue` 在格式化数字值时使用的可选后缀,并被识别为有效并从输入字符串(分配给 `StringValue`)中剥离。
string StringValue 获取或设置数字值的 `string` 表示形式。当构造 `DecimalStringBinder` 对象时,可以将其绑定到外部 `string` 属性。
string UndecoratedStringValue 获取当前 `StringValue`,其中已剥离 `StringPrefix` 和 `StringSuffix`。
decimal DecimalValue 获取或设置数字值。可以将其绑定到以下类型的外部数字属性:`decimal`、`double`、`long` 或 `int`……。绑定必须通过 `DecimalStringBinder` 构造函数创建。
bool IsValid 获取一个值,指示 `StringValue` 的当前值是否是数字值的有效表示……
object DecimalPropertyParentObject 获取包含数字属性(如果存在)的对象(如果存在),该数字属性被绑定到 `DecimalStringBinder` 对象的 `DecimalValue` 属性。
PropertyInfo DecimalProperty 获取有关被绑定到 `DecimalStringBinder` 对象的 `DecimalValue` 属性的数字属性(如果存在)的反射信息。
object StringPropertyParentObject 获取包含字符串属性(如果存在)的对象(如果存在),该字符串属性被绑定到 `DecimalStringBinder` 对象的 `StringValue` 属性。
PropertyInfo StringProperty 获取有关被绑定到 `DecimalStringBinder` 对象的 `StringValue` 属性的字符串属性(如果存在)的反射信息。

事件

`DecimalStringBinder` 实现 `INotifyPropertyChanged` 接口,因此当上述任何属性发生更改时,它会公开并引发 `PropertyChanged` 事件。

方法

此外,`DecimalStringBinder` 还提供了一个 `public` 方法

void RefreshStringValue 将 `StringValue` 属性设置为 `DecimalValue` 属性值的规范表示。

更广泛和详细的文档可在源代码中以及通过 Visual Studio 中的 Intellisense 获取。从上面列出的功能和简要描述中,我希望读者能够开始了解该类试图做什么,它的行为可以通过属性轻松参数化,以及它在各种场景中有多有用。`DecimalStringBinder` 提供了一套相当全面的功能,以支持单向绑定(可能带有特殊格式)或双向绑定。

在浏览本文附带的可下载代码时,您还会找到一些有用的方法和一个小型但有趣的辅助类 `NeverNullString`,我希望它们能很好地解释自己。

代码包含两个项目

  1. 一个“沙盒”控制台应用程序,用于在非常受控的环境中方便地测试 `DecimalStringBinder` 的功能,并发现或验证各种属性设置的结果。
  2. 一个 Silverlight 应用程序,展示了 `DecimalStringBinder` 对象在实际应用程序场景中可能如何使用。

强烈建议对控制台应用程序采取立即动手、实验的方法。下面将讨论 Silverlight 应用程序暴露的问题和解决方案。

关注点

有时,数字值可能通过公式相关,因此更改一个值会导致一个或多个其他值更改。这种情况可能会带来一些有趣的问题。设想用户可以输入一对由公式关联的货币值——一个值代表以千克或磅为单位计量的商品的重量,另一个值代表每单位重量的价格。当用户修改其中一个值时,另一个值将根据用户输入的值自动计算。货币值之间关系的公式可以表示为

  • 每单位价格 = 每件商品价格 / 每件商品单位数,四舍五入到小数点后 4 位。
  • 每件商品价格 = 每单位价格 * 每件商品单位数,向下舍入到小数点后 2 位。

我们还假设公式右侧某个项目的更改会自动调用对左侧项目的重新计算。这个规范似乎是合乎逻辑的,但某些实现会导致一个小问题。

问题 arises because it's not possible to represent numeric values with an infinite number of decimal places, and the problem becomes especially obvious when we modify values by rounding them to just a few decimal places. Currency values, for example, are often rounded to two decimal places.

在上述示例中,如果用户输入每件商品的价格,则每单位价格会重新计算——由于每单位价格因此而改变,因此可以响应地重新计算每件商品的价格,很可能产生一个与用户刚输入的值不同的值。通常,这种“猜测用户意图”的行为是不合适的。

此外,在没有特定干预的情况下,从 `TextBox.Text` 更新绑定的字符串(至少在 Silverlight 中)发生在 `TextBox` 失去焦点时。如果用户输入的值在 `TextBox` 失去焦点时发生更改,这可能不好。

也许我们希望用户在修改数字数据时,逐个字符地反映用户的输入。然而,如果我们朝着这个目标努力,那么前面提到的问题就会变得无法容忍,因为应用程序可能会在用户键入时更改用户的输入!

通过为 `TextBox` 的 `KeyUp` 事件(`TextChanged` 也可以达到此目的)添加处理程序,我们可以同时解决这两个问题。每当发生任何更改时,都明确且立即地从 `TextBox.Text` 进行绑定,导致数字数据发生更改,并根据用户更改了哪个值来重新运行相应的计算。下面的代码(在 _MainView.xaml.cs_ 中找到)实现了这一点。名称后缀为“`Dsb`”的 `ViewModel` 属性是 `DecimalStringBinder` 对象,它们通过 View 的 XAML 中的绑定语句绑定到 `TextBox.Text` 属性。

private void TextBox_KeyUp(object sender, TextChangedEventArgs e)
{
    if (e.Key == Key.Tab && sender is TextBox)
    {
        // Select all text when the user tabs into a 
        // TextBox that contains a numeric value.
        ...
    }
    else
    {
        TextBox sendingTextBox = (TextBox)sender;
        switch (sendingTextBox.Name)
        {
            case "WeightTextBox":
                _viewModel.WeightDsb.StringValue = sendingTextBox.Text;
                _viewModel.CalculateTotalPrice();
                break;
            case "UnitPriceTextBox":
                _viewModel.UnitPriceDsb.StringValue = sendingTextBox.Text;
                _viewModel.CalculateTotalPrice();
                break;
            case "TotalPriceTextBox":
                _viewModel.TotalPriceDsb.StringValue = sendingTextBox.Text;
                _viewModel.CalculateUnitPrice();
                break;
        }
    }
} 

另一个值得一提的 UI 交互的细微差别是,当 `TextBox` 失去焦点时,用户的输入格式会被“清理”以符合规范的显示格式(如果需要,通过截断或扩展)。这是通过处理 `LostFocus` 事件实现的,如下所示

private void TextBox_LostFocus(object sender, System.Windows.RoutedEventArgs e)
{
    TextBox sendingTextBox = (TextBox)sender;
    switch (sendingTextBox.Name)
    {
        case "WeightTextBox":
            _viewModel.WeightDsb.RefreshStringValue();
            break;
        case "UnitPriceTextBox":
            _viewModel.UnitPriceDsb.RefreshStringValue();
            break;
        case "TotalPriceTextBox":
            _viewModel.TotalPriceDsb.RefreshStringValue();
            break;
    }
}

评估上述功能组合可用性的最佳方法是运行并试用 Silverlight 演示应用程序。我的同事和我发现,业务用户喜欢与包含所提供功能的 UI 进行交互,而且我们不需要购买专门的第三方解决方案来解决将数字数据绑定到用户可修改的 `string`(例如 `TextBox.Text`)的问题。

历史

  • 2010 年 4 月 25 日:初始版本
© . All rights reserved.