天天看點

[UWP]了解IValueConverter

原文: [UWP]了解IValueConverter

1. 前言

IValueConverter是用于資料綁定的強大的武器,它用于Value在Binding Source和Binding Target之間的轉換。本文将介紹IValueConverter的用法及一些常用的實作。

2. 為什麼要使用IValueConverter

假設有如下的類TestResult:

public class TestResult
{
    public bool Passed { get; set; }

}
           

UI需要通過Passed這個屬性決定顯示結果的文字顔色為紅色或綠色,一般初學者最常見的做法是修改TestResult類,添加一個和Passed相關的屬性:

public class TestResult
{
    public bool Passed { get; set; }

    public Brush TestResultBrush
    {
        get
        {

            if (Passed)
                return new SolidColorBrush(Colors.Red);
            else
                return new SolidColorBrush(Colors.Green);
        }
    }
}
           

然後在XAML上綁定到這個屬性:

<TextBlock  Text="Score : 60" Foreground="{Binding TestResultBrush}"/>           

另一種做法是直接才Code Behind為TextBlock更改Foreground:

var testResult = DataContext as TestResult;
if (testResult != null)
{
    if (testResult.Passed)
        ResultElement.Foreground = new SolidColorBrush(Colors.Red);
    else
        ResultElement.Foreground = new SolidColorBrush(Colors.Green);
}
           

兩種做法都不夠優雅,可以指出一大堆問題:破壞了TestResult的結構,違反了開放封閉原則,令UI和資料太過耦合,太多Hard Code等。

這種情況通常都可以使用IValueConverter處理。在Binding中,IValueConverter可以用于資料呈現前将它轉換成新的目标值,實作IValueConverter需要執行以下步驟:

  1. 建立一個實作了IValueConverter接口的類類;
  2. 實作

    public object Convert(object value, Type targetType, object parameter, string language)

    方法,該方法将資料轉換為新目标值;
  3. public object ConvertBack(object value, Type targetType, object parameter, string language)

    ,該方法執行反向轉換,隻有使用雙向綁定才需要實作這個方法。

在這個例子裡,IValueConverter的目的是将bool類型的Passed轉換成Brush,實作如下:

public class BoolToBrushConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value is bool passed)
            return new SolidColorBrush(passed ? Colors.Green : Colors.Red);

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}
           

在XAML中使用這個Convnerter需要先将它定義為Resource,然後Binding中指定Converter到這個已定義的Resource:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.Resources>
        <local:BoolToBrushConverter x:Key="BoolToBrushConverter"/>
    </Grid.Resources>
    <TextBlock  Text="Score : 60" Foreground="{Binding Passed,Converter={StaticResource BoolToBrushConverter}}"/>
</Grid>
           

3. BoolToValueConverter

在XAML漫長的曆史裡,IValueConverter也誕生了各種奇怪的技巧,其中最常用的是BoolToValueConverter。

public class BoolToValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value == null || (bool) value == false)
            return DependencyProperty.UnsetValue;

        return parameter;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        return Equals(value, parameter);
    }
}
           

BoolToValueConverter靈活使用了Binding中

ConverterParameter

FallbackValue

兩個參數,常常用于解決IValueConverter中HardCode的問題。在Binding中,FallbackValue指明了如果Binding沒法傳回任何值時使用的值,在IValueConverter中傳回

DependencyProperty.UnsetValue

即告訴Binding要使用FallbackValue的值。

使用BoolToValueConverter解決了上述例子的Hard Code的問題,在XAML中使用如下:

<Grid>
    <Grid.Resources>
        <local:BoolToValueConverter x:Key="BoolToValueConverter"/>
    </Grid.Resources>
    <TextBlock Text="Score : 60" Foreground="{Binding Passed,Converter={StaticResource BoolToValueConverter},ConverterParameter=Green,FallbackValue=Red}"/>
</Grid>
           

4. BoolToObjectConverter

需要注意的是上面XAML中Green和Red都隻是字元串,它們最終能被解析成SolidColorBrush是由于

TypeConveter

的支援,也就是說上述XAML文法隻能用于TypeConverter支援的資料類型,而且這種寫法還是太過HardCode。如果要支援複雜類型或者對應本地化等問題,可以将ConverterParameter和FallbackValue綁定到StaticResource :

<Grid.Resources>
    <SolidColorBrush Color="Green" x:Key="PassedBrush"/>
    <SolidColorBrush Color="Red" x:Key="FailedBrush"/>
    <local:BoolToValueConverter x:Key="BoolToValueConverter"/>
</Grid.Resources>
<TextBlock Text="Score : 60" Foreground="{Binding Passed,Converter={StaticResource BoolToValueConverter},ConverterParameter={StaticResource PassedBrush},FallbackValue={StaticResource FailedBrush}}"/>
           

雖然看上去是很靈活,但如果有大量傳回同樣值的BoolToValueConverter将會使XAML産生大量備援。

UWP Community Toolkit

提供了一些常用的IValueConverter實作,其中最常用的是

BoolToObjectConverter

。BoolToObjectConverter和BoolToValueConverter功能類似,但它提供了

public object TrueValue { get; set; }

public object FalseValue { get; set; }

兩個屬性,而且這兩個屬性是依賴屬性,可以使用綁定為其指派。使用如下:

<Grid.Resources>
    <SolidColorBrush Color="Green" x:Key="PassedBrush"/>
    <SolidColorBrush Color="Red" x:Key="FailedBrush"/>
    <converters:BoolToObjectConverter x:Key="BoolToObjectConverter" TrueValue="{StaticResource PassedBrush}" FalseValue="{StaticResource FailedBrush}"/>
</Grid.Resources>
<TextBlock Text="Score : 60" Foreground="{Binding Passed,Converter={StaticResource BoolToObjectConverter}}"/>
           

5. BoolToVisibilityConverter

UWP Community Toolkit中提供了另一個常用的Converter:

BoolToVisibilityConverter

。這個Converter隻是簡單地繼承了BoolToObjectConverter,并且為TrueValue和FalseValue設定了預設值:

public BoolToVisibilityConverter()
{
    TrueValue = Visibility.Visible;
    FalseValue = Visibility.Collapsed;
}
           

BoolToVisibilityConverter雖然簡單,但确實好用。不過從1607以後就不需要這個Converter了,微軟是

這樣

說的:

從 Windows 10 版本 1607 開始,XAML 架構向 Visibility 轉換器提供内置布爾值。 轉換器将 true 映射到 Visible 枚舉值并将 false 映射到 Collapsed,以便你可以将 Visibility 屬性綁定到布爾值,而無需建立轉換器。 若要使用内置轉換器,你的應用的最低目标 SDK 版本必須為 14393 或更高版本。

但有時候反而需要True對應Collapsed,于是現在是另一個常用Converter -

BoolNegationConverter

登上曆史舞台的時候了:

<StackPanel >
    <StackPanel.Resources>
        <converters:BoolNegationConverter x:Key="BoolNegationConverter" />
    </StackPanel.Resources>
    <TextBlock Text="Passed" Foreground="Green" Visibility="{Binding Passed}"/>
    <TextBlock Text="Failed" Foreground="Red" Visibility="{Binding Passed,Converter={StaticResource BoolNegationConverter}}"/>
</StackPanel>
           

6. StringFormatConverter

UWP的Binding缺少了StringFormat,這對Binding産生了很大影響,為彌補這個缺陷,可以使用UWP Community Toolkit中的

StringFormatConverter

。它的代碼也十分簡單(其實這才是ConverterParameter的正确用法):

public object Convert(object value, Type targetType, object parameter, string language)
{
    if (value == null)
    {
        return null;
    }

    if (parameter == null)
    {
        return value;
    }

    return string.Format((string)parameter, value);
}
           

在XAML中使用如下:

<TextBlock Text="{Binding DoubleValue,Converter={StaticResource StringFormatConverter},ConverterParameter='{}{0:N2}'}"/>
<TextBlock Text="{Binding DoubleValue,Converter={StaticResource StringFormatConverter},ConverterParameter='There are {0:N0} Items'}"/>
           

結果如下:

[UWP]了解IValueConverter

除了彌補StringFormat的功能,StringFormatConverter還有其它的應用場景。

** TestModel.CS **

public IEnumerable<ClickMode> ClickModes => new List<ClickMode> { ClickMode.Hover, ClickMode.Press, ClickMode.Release };           
<ListBox ItemsSource="{Binding ClickModes}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding }" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
<ListBox ItemsSource="{Binding ClickModes}"/>           

在WPF中,以上XAML都可以正常呈現,而在UWP中,以上XAML顯示如下:

[UWP]了解IValueConverter

這種情況可以使用StringFormatConverter顯示枚舉的名稱:

<ListBox ItemsSource="{Binding ClickModes}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Converter={StaticResource StringFormatConverter}}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
           
[UWP]了解IValueConverter

可以說對UWP來說StringFormatConverter十分必要。

7. language參數

public object Convert(object value, Type targetType, object parameter, string language)

方法中的參數language通常用于本地化,例如可以建立一個DateTimeValueConverter:

public class DateTimeValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value is DateTime dateTime)
        {
            var culture = new CultureInfo(language);
            return dateTime.ToString(culture.DateTimeFormat);
        }
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}
           
<TextBlock Text="{Binding Date,Converter={StaticResource DateTimeValueConverter},ConverterLanguage=en-US}"/>
<TextBlock Text="{Binding Date,Converter={StaticResource DateTimeValueConverter},ConverterLanguage=zh-CN}"/>
           
[UWP]了解IValueConverter

8. targetType參數

targetType參數指轉換後的目标類型,使用這個參數可以實作一個簡單的Value Converter:

public class ValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        return System.Convert.ChangeType(value, targetType);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}
           

雖然代碼簡單,但它可以解決不少問題,例如

了解TypeConverter

這篇文章裡提到的不能在XAML中使用decimal的問題。IValueConverter要起作用依賴于BindingSource,而在XAML中雖然很多東西都可以用來做BindingSource,例如用元素自己的Tag:

<local:MyContentControl Tag="10.01" Amount="{Binding Converter={StaticResource ValueConverter},Path=Tag,RelativeSource={RelativeSource Mode=Self}}"/>           

或者Resources中的字元串:

<Grid.Resources>
    <x:String x:Key="DecimalString">10.01</x:String>
</Grid.Resources>
<local:MyContentControl Amount="{Binding Source={StaticResource DecimalString},Converter={StaticResource ValueConverter}}"/>
           

或者更進一步寫一個字元串的包裝類:

public class StringWrapper
{
    public string this[string key]
    {
        get
        {
            return key;
        }
    }
}
           
<local:MyContentControl Amount="{Binding [10.01],Source={StaticResource StringWrapper},Converter={StaticResource ValueConverter}}"/>
           

9. 使用IValueConverter的其它經驗

9.1 統一管理IValueConverter

由于大部分IValueConverter行為是固定的,通常我都會把常用的IValueConverter放到一個Converters.xaml,然後在App.xaml中年合并資源字典,這樣不用重複寫建立Converter的xaml,也避免了重複建立Converter的資源消耗:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Converters.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>
           

9.2 格式化

[UWP]了解IValueConverter

Binding最讓人诟病的缺點就是它的文法太長太長太長,例如以上兩個TextBlock,在IDE中很難判斷這它們有什麼不同。很多時候我都會把XAML的格式化設定成“将每個屬性分行放置”,如下圖:

[UWP]了解IValueConverter

這樣上面兩個TextBlock的XAML就清晰許多了:

[UWP]了解IValueConverter

不過這樣設定也并不全是好處,怎麼設定具體還是看個人喜好和螢幕尺寸。

10. 結語

雖然IValueConverter的文章已經不少了,但還是常常見到亂來的IValueConverter實作,而且UWP的IValueConverter有一些改變,是以還是寫了這篇文章。

我很想寫一些常用的,或者容易用錯的基礎知識,但連IValueConverter都不知不覺就寫得這麼長了,實在沒勇氣寫Binding的概念,何況關于Binding 已經有很多很實用的文章。

我十厘清楚文章寫得太長就會被“儲存到Pocket”,我也想每篇文章都能在三五分鐘内看完,但偏偏越基礎的概念就越能寫得長,而且寫得簡短些又會被移出部落格園首頁,很難把握尺度。

下一篇文章會盡量寫短一些。

11. 參考

IValueConverter Interface Binding Class 深入了解資料綁定 Converters - UWP Community Toolkit _ Microsoft Docs