概要
WPFでViewModel⇔View間でデータを変換する際にはIValueConverterを継承したConverterを定義することがよくあります。
しかしこのIValueConverterは特定の型間の変換に用いることがほとんどなのにもかかわらず、
object
型で入出力するので書きづらいしエラーにも気づきづらいです。
そこで、Generic型を使用した汎用抽象Converterクラス経由で継承することでこの問題を解決します。
実行結果
題材とするデモアプリの実行結果です。
起動時に青い背景のTextBlockに長い文字列があり見切れています。
下のIsTrim
をチェックすると文字列の見切れている部分が...
と表示されます。
IsTrim
の代わりにその下のIsWrapp
をチェックすると文字列が折り返して表示されます。
デモアプリコード
デモアプリのコードです。
MainWindowは主に以下の3つで構成されています。
- 2つのプロパティがBindingされた固定された文字列の青背景のTextBlock
- ViewModelの
IsTrim
とBindingされたCheckBox - ViewModelの
IsWrapp
とBindingされたCheckBox
本題となるBoolToTextTrimmingConverter
とBoolToTextWrappingConverter
については後で述べます。
<Window
x:Class="GenericConverterTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GenericConverterTest"
Width="300"
Height="250">
<Window.Resources>
<local:BoolToTextTrimmingConverter x:Key="BoolToTextTrim" />
<local:BoolToTextWrappingConverter x:Key="BoolToTextWrap" />
</Window.Resources>
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<StackPanel Margin="30">
<TextBlock
Width="100"
Height="100"
Background="LightSkyBlue"
Text="LOOONG LOOONG LOOONG LOOONG TEXT"
TextTrimming="{Binding IsTrim, Converter={StaticResource BoolToTextTrim}}"
TextWrapping="{Binding IsWrapp, Converter={StaticResource BoolToTextWrap}}" />
<CheckBox Content="IsTrim" IsChecked="{Binding IsTrim}" />
<CheckBox Content="IsWrapp" IsChecked="{Binding IsWrapp}" />
</StackPanel>
</Window>
ViewModelは以下の2つ
- bool型プロパティ
IsTrim
- bool型プロパティ
IsWrapp
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private bool _IsTrim;
public bool IsTrim
{
get => _IsTrim;
set
{
_IsTrim = value;
RaisePropertyChanged();
}
}
private bool _IsWrapp;
public bool IsWrapp
{
get => _IsWrapp;
set
{
_IsWrapp = value;
RaisePropertyChanged();
}
}
}
問題点
上記Viewで使われていたBoolToTextTrimmingConverter
のコードが以下です。
[ValueConversion(typeof(bool), typeof(TextTrimming))]
public class BoolToTextTrimmingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? TextTrimming.CharacterEllipsis : TextTrimming.None;
//返り値の型が間違っていてもエラーしない
//return (bool)value ? TextWrapping.Wrap : TextWrapping.NoWrap;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return !((TextTrimming)value == TextTrimming.None);
}
}
BoolをTextTrimming(文字列が収まらないときに...
を最後に表示するかの指定)に変換しています。
ここでは以下の問題があります。
- 入力値のValueがobjectなので毎回型変換する
- 返り値の型が間違っていてもobject型なのでコンパイルエラーしない
解決策
Generic型を使用して汎用の抽象Converterクラスを使用します。
IValueConverterのメソッドからGeneric型を使用した別のメソッドに変換します
変換前後がobject型だったのが別々のGeneric型になります。
public object Convert(object value, ...
↓
public TTarget Convert(TSource value, ...
/// <summary>
/// Generic型を使用した汎用コンバーター抽象クラス
/// </summary>
/// <typeparam name="TSource">バインディング ソース型</typeparam>
/// <typeparam name="TTarget">バインディング ターゲット型</typeparam>
public abstract class GenericConverter<TSource, TTarget> : IValueConverter
{
/// <summary>
/// IValueConverterのConvertメソッド実装(Generic型にキャストして抽象メソッドConvertを呼び出す)
/// </summary>
/// <param name="value">バインディング ソースによって生成された値</param>
/// <param name="targetType">バインディング ターゲット プロパティの型</param>
/// <param name="parameter">使用するコンバーター パラメーター</param>
/// <param name="culture">コンバーターで使用するカルチャ</param>
/// <returns>変換された値</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> Convert((TSource)value, parameter, culture);
/// <summary>
/// Generic型を使用して値変換する抽象メソッド
/// </summary>
/// <param name="value">バインディング ソースによって生成された値</param>
/// <param name="parameter">使用するコンバーター パラメーター</param>
/// <param name="culture">コンバーターで使用するカルチャ</param>
/// <returns>変換された値</returns>
public abstract TTarget Convert(TSource value, object parameter, CultureInfo culture);
/// <summary>
/// IValueConverterのConvertBackメソッド実装(Generic型にキャストして抽象メソッドConvertBackを呼び出す)
/// </summary>
/// <param name="value">バインディング ターゲットによって生成された値</param>
/// <param name="targetType">変換後の型</param>
/// <param name="parameter">使用するコンバーター パラメーター</param>
/// <param name="culture">コンバーターで使用するカルチャ</param>
/// <returns>変換された値</returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> ConvertBack((TTarget)value, parameter, culture);
/// <summary>
/// Generic型を使用して値変換する抽象メソッド
/// </summary>
/// <param name="value">バインディング ターゲットによって生成された値</param>
/// <param name="parameter">使用するコンバーター パラメーター</param>
/// <param name="culture">コンバーターで使用するカルチャ</param>
/// <returns>変換された値</returns>
public abstract TSource ConvertBack(TTarget value, object parameter, CultureInfo culture);
}
これを継承した具象クラスです。
退屈なキャストは終わっており、変換する双方の型が明示されているので、書きやすいです。
[ValueConversion(typeof(bool), typeof(TextTrimming))]
public class BoolToTextTrimmingConverter : GenericConverter<bool, TextTrimming>
{
public override TextTrimming Convert(bool value, object parameter, CultureInfo culture)
{
return value ? TextTrimming.CharacterEllipsis : TextTrimming.None;
//返り値の型が間違っているのでエラーする
//return (bool)value ? TextWrapping.Wrap : TextWrapping.NoWrap;
}
public override bool ConvertBack(TextTrimming value, object parameter, CultureInfo culture)
{
return !(value == TextTrimming.None);
}
}
[ValueConversion(typeof(bool), typeof(TextWrapping))]
class BoolToTextWrappingConverter : GenericConverter<bool, TextWrapping>
{
public override TextWrapping Convert(bool value, object parameter, CultureInfo culture)
{
return value ? TextWrapping.Wrap : TextWrapping.NoWrap;
}
public override bool ConvertBack(TextWrapping value, object parameter, CultureInfo culture)
{
return !(value == TextWrapping.NoWrap);
}
}
CodeSnippet
さらに書きやすくするため、このGenericConverterを継承したConverterクラスのコードスニペットを使用します。
GenericConverter.snippet -GitHub
ショートカットはgconv
です。
変換元と変換先の型を入力すると自動でConverterクラスのスケルトンを作成します。
クラス名も ソース型Toターゲット型Converter
になります。
ついでによく使う名前空間(System.Windows
等)をUsingに追加します。
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2008/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>GenericConverter</Title>
<Shortcut>gconv</Shortcut>
<Author>soi</Author>
<Description>Generic型を使用したGenericConverter継承したクラスを作成します</Description>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Imports>
<Import><Namespace>System.Globalization</Namespace></Import>
<Import><Namespace>System.Windows</Namespace></Import>
<Import><Namespace>System.Windows.Controls</Namespace></Import>
<Import><Namespace>System.Windows.Data</Namespace></Import>
<Import><Namespace>System.Windows.Media</Namespace></Import>
</Imports>
<Declarations>
<Literal>
<ID>typeSource</ID>
<ToolTip>プロパティの型</ToolTip>
<Default>bool</Default>
</Literal>
<Literal>
<ID>typeTarget</ID>
<ToolTip>プロパティの型</ToolTip>
<Default>Visibility</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[
[ValueConversion(typeof($typeSource$), typeof($typeTarget$))]
public class $typeSource$To$typeTarget$Converter : GenericConverter<$typeSource$, $typeTarget$ >
{
public override $typeTarget$ Convert($typeSource$ value, object parameter, CultureInfo culture)
{
return default($typeTarget$);
}
public override $typeSource$ ConvertBack($typeTarget$ value, object parameter, CultureInfo culture)
{
return default($typeSource$);
}
}
]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
コードスニペットの使用動画
こんな風にリズムよくコーディングができて楽しい!
環境
VisualStudio2017
.NET Framework 4.7
C#7.1