类型不匹配的数据绑定

已完成

有时,所使用的数据与显示数据的控件属性的数据类型不匹配。 例如,你可能有一个货币值存储在要显示在 Label 控件上的 decimal 类型中,其格式为货币。 一个更复杂的示例是模块中提供的天气应用。 图像应根据天气预报的 SunnyCloudy 枚举值显示,但你无法将源的枚举值绑定到目标的图像属性。 本单元着眼于转换数据的方法。

字符串格式设置

常见的类型不匹配是想要显示为格式字符串的内部类型。 就像要显示 DateTime 值的一部分,或者想要将 decimal 类型格式化为货币时一样。

假设你想要显示账单上的到期金额,并且你的数据对象上有此属性:

public decimal BillAmount { get; set; }

欠款金额最终为 22.0304。 可以使用两个标签控件来显示一些文本和美元金额,如以下代码片段所示:

<HorizontalStackLayout>
    <Label Text="You owe" Margin="0,0,5,0" />
    <Label Text="{Binding BillAmount}" />
    <Label Text="to the bank" Margin="5,0,0,0" />
</HorizontalStackLayout>

这会向 UI 输出一个类似于 You owe 22.0304 to the bank 的字符串,但它缺少货币符号,并且根据本地货币,小数位数可能过多或过少。 通常,你会在代码中使用“C”(或货币)格式说明符将值作为字符串处理,如下所示:

string formattedBillAmount = string.Format("{0:C}", BillAmount);

但是要在数据绑定中使用格式设置,必须让数据对象提供格式化字符串作为不同的属性,或者以某种方式拦截它并自行设置格式。 幸运的是,.NET MAUI 绑定提供了一种使用 StringFormat 绑定属性格式化字符串的方法。 格式字符串遵循与 String.Format 方法相同的规则。 使用单引号将格式字符串括起来,这样 XAML 分析器就不会因为大括号而混淆了。 字符串格式参数 0 是绑定处理的属性值。

<Label Text="{Binding BillAmount, StringFormat='You owe {0:C} to the bank'}" />

请考虑以下 XAML,它演示了显示 BillAmount 的两种方式:

<VerticalStackLayout Padding="10">
    <HorizontalStackLayout>
        <Label Text="You owe" Margin="0,0,5,0" />
        <Label Text="{Binding BillAmount}" />
        <Label Text="to the bank" Margin="5,0,0,0" />
    </HorizontalStackLayout>
    <HorizontalStackLayout>
        <Label Text="{Binding BillAmount, StringFormat='You owe {0:C} to the bank'}" />
    </HorizontalStackLayout>
</VerticalStackLayout>

下图说明了 XAML 输出在屏幕上生成的内容:

Screenshot of a .NET MAUI Android app displaying two different label controls with text related to owing a bill. One label's text is formatted with USD currency.

使用 StringFormat 绑定属性的 XAML 比其他 XAML 更简单,并且你有权访问 .NET 强大的字符串格式化系统。

自定义类型转换

当将值显示为字符串时,StringFormat 绑定属性很方便,但当你想要从其他类型转换 ColorImage 等内容时,则不太方便。 在这些情况下,需要编写自定义转换代码。

假设你提示用户选择密码,并且希望在 UI 中使用颜色来指示密码的强度。 强度分为三个等级:弱、良、强。 强度取决于密码中包含的字符数。 为了向用户提供有关其密码强度的即时反馈,需要包含密码的 Entry 控件的背景根据强度进行更改。 下图展示了这三个强度级别:弱、良和强。

Screenshot of a .NET MAUI Android app with three entry controls, each with a different colored background.

在屏幕截图中的三个输入控件中,第一个输入了四个字符并具有红色背景。 第二个输入了九个字符,并具有黄色背景。 最后一个输入控件有 15 个字符,具有蓝色背景。

这些级别分配给 Strength 枚举:

private enum Strength
{
    Weak,
    Good,
    Strong
}

数据对象被分配为页面的 BindingContext,其中包含带有 PasswordStrength 属性的密码强度。 当用户输入密码时,PasswordStrength 属性会根据密码的长度进行更改。 由于数据对象包含 PasswordStrength 属性,因此将该属性绑定到 Entry 控件的 BackgroundColor

<Entry BackgroundColor="{Binding PasswordStrength} ... />

但这里有一个问题。 PasswordStrength 的类型为 Strength,而 BackgroundColor 的类型为 Color。 这些类型彼此不兼容。 .NET MAUI 提供了一种解决这些类型不匹配问题的方法,即绑定的 Converter 属性。

绑定转换器,顾名思义,就是在绑定源和目标之间进行转换。 转换器通过 IValueConverter 接口实施。

实现 IValueConverter

你将转换逻辑创建到实施 IValueConverter 接口的类中。 通常,这些类的名称以名称 Converter 结尾,以明确其用途。

IValueConverter 接口定义两种方法:

  • Convert - 从绑定源的属性转换为绑定目标的 UI 属性。

  • ConvertBack - 从绑定目标的 UI 属性转换回绑定源的属性。

    这种方法很少使用,在这种情况下也不会使用。 大多数转换器会抛出 NotSupportedException 来指示不支持此转换。

下面是接口的协定:

public interface IValueConverter
{
    object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture);
    object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture);
}

回想一下,我们正在使用的场景是将 Entry.BackgroundColor 属性绑定到数据对象的 PasswordStrength 属性,该属性是一个 Strength 枚举。

<Entry BackgroundColor="{Binding PasswordStrength} ... />

Strength 枚举需要转换为 Color。 因此,转换器需要评估提供的 Strength 值,并返回不同的 Color。 下面的代码演示了这种转换:

namespace MyProject.Converters;

class StrengthToColorConverter : IValueConverter
{
    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        return (Strength)value! switch
        {
            Strength.Weak => Colors.OrangeRed,
            Strength.Good => Colors.Yellow,
            Strength.Strong => Colors.LightBlue,
            _ => Colors.LightBlue
        };
    }

    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
        throw new NotImplementedException();
}

让我们分解一下这段代码:

  • Convert 方法有四个参数。 通常可以放弃最后两个参数,除非出于特定原因需要使用它们。
  • value 参数包含传入值。 在此示例中,它是一个 Strength 枚举值。
  • 忽略 targetType 参数。 但是,可以使用此参数来验证转换器所使用的属性类型是否为 Color。 为了简单起见,本示例中省略了这一点。
  • switch 表达式用于根据 Strength 值返回不同的颜色。

在 XAML 中使用转换器

创建转换器类后,需要创建它的实例并在绑定中引用它。 实例化转换器的标准方法是在根元素的资源字典中。

首先,将 XML 命名空间映射到包含转换器的 .NET 命名空间。 在以下代码示例中,cvt XML 命名空间映射到 MyProject.Converters .NET 命名空间:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:cvt="clr-namespace:MyProject.Converters"
             ...

接下来,在 ContentPage.Resources 中创建一个实例:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:cvt="clr-namespace:MyProject.Converters"
             x:Class="MyProject.PasswordExample">
    <ContentPage.Resources>
        <cvt:StrengthToColorConverter x:Key="StrengthToColorConverter" />
    </ContentPage.Resources>

现在,转换器的实例位于资源字典中,其密钥为 StrengthToColorConverter,恰好与类型同名。 这是命名转换器的典型方式,因为你通常只有一个在整个 XAML 中重复使用的转换器。 如果出于某种原因需要多个转换器实例,则它们之间的密钥必须不同。

最后,在绑定时引用转换器。 由于转换器位于资源字典中,因此可以使用 {StaticResource} 标记扩展来引用它。 该转换器被分配给 Converter 绑定属性:

<Entry BackgroundColor="{Binding PasswordStrength, Converter={StaticResource StrengthToColorConverter}}" ... />

知识检查

1.

若要在数据绑定中显示值并将其格式化为字符串,最好的方法是什么?

2.

数据绑定转换时使用哪种类型?