1. 前言 IValueConverter是用於數據綁定的強大的武器,它用於Value在Binding Source和Binding Target之間的轉換。本文將介紹IValueConverter的用法及一些常用的實現。 2. 為什麼要使用IValueConverter 假設有如下的類TestRe ...
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需要執行以下步驟:
- 創建一個實現了IValueConverter介面的類類;
- 實現
public object Convert(object value, Type targetType, object parameter, string language)
方法,該方法將數據轉換為新目標值; - 實現
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'}"/>
結果如下:
除了彌補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顯示如下:
這種情況可以使用StringFormatConverter顯示枚舉的名稱:
<ListBox ItemsSource="{Binding ClickModes}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource StringFormatConverter}}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
可以說對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}"/>
結果如下:
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 格式化
Binding最讓人詬病的缺點就是它的語法太長太長太長,例如以上兩個TextBlock,在IDE中很難判斷這它們有什麼不同。很多時候我都會把XAML的格式化設置成“將每個屬性分行放置”,如下圖:
這樣上面兩個TextBlock的XAML就清晰許多了:
不過這樣設置也並不全是好處,怎麼設置具體還是看個人喜好和屏幕尺寸。
10. 結語
雖然IValueConverter的文章已經不少了,但還是常常見到亂來的IValueConverter實現,而且UWP的IValueConverter有一些改變,所以還是寫了這篇文章。
我很想寫一些常用的,或者容易用錯的基礎知識,但連IValueConverter都不知不覺就寫得這麼長了,實在沒勇氣寫Binding的概念,何況關於Binding 已經有很多很實用的文章。
我十分清楚文章寫得太長就會被“保存到Pocket”,我也想每篇文章都能在三五分鐘內看完,但偏偏越基礎的概念就越能寫得長,而且寫得簡短些又會被移出博客園首頁,很難把握尺度。
下一篇文章會儘量寫短一些。
11. 參考
IValueConverter Interface
Binding Class
深入瞭解數據綁定
Converters - UWP Community Toolkit _ Microsoft Docs