Generic型を使用した汎用Converter（CodeSnippet付き）

# 概要
WPFでViewModel⇔View間でデータを変換する際にはIValueConverterを継承したConverterを定義することがよくあります。
しかしこのIValueConverterは特定の型間の変換に用いることがほとんどなのにもかかわらず、
`object`型で入出力するので書きづらいしエラーにも気づきづらいです。

そこで、Generic型を使用した汎用抽象Converterクラス経由で継承することでこの問題を解決します。

# 実行結果
題材とするデモアプリの実行結果です。
起動時に青い背景のTextBlockに長い文字列があり見切れています。
![スクリーンショット 2018-03-15 22.39.06.png](https://qiita-image-store.s3.amazonaws.com/0/120077/fd7e651f-342e-9c8d-3387-d81279e54447.png)

下の`IsTrim`をチェックすると文字列の見切れている部分が`...`と表示されます。
![スクリーンショット 2018-03-15 22.39.09.png](https://qiita-image-store.s3.amazonaws.com/0/120077/8904c876-4f12-e8f0-0c82-0c32ea8baa59.png)

`IsTrim`の代わりにその下の`IsWrapp`をチェックすると文字列が折り返して表示されます。
![スクリーンショット 2018-03-15 22.39.17.png](https://qiita-image-store.s3.amazonaws.com/0/120077/73708c54-ce37-d12b-21c1-f352a0ded82a.png)

# デモアプリコード
デモアプリのコードです。
MainWindowは主に以下の３つで構成されています。

- 2つのプロパティがBindingされた固定された文字列の青背景のTextBlock
- ViewModelの`IsTrim`とBindingされたCheckBox
- ViewModelの`IsWrapp`とBindingされたCheckBox

本題となる`BoolToTextTrimmingConverter`と`BoolToTextWrappingConverter`については後で述べます。

```xml:MainWindow.xaml
<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`

```csharp:MainWindowViewModel.cs
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`のコードが以下です。

```BoolToTextTrimmingConverter.cs
[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型になります。

```cs
public object Convert(object value, ...
↓
public TTarget Convert(TSource value, ...
```

```GenericConverter.cs
/// <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);
}
```

これを継承した具象クラスです。
退屈なキャストは終わっており、変換する双方の型が明示されているので、書きやすいです。

```BoolToTextTrimmingConverter.cs
[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);
    }
}
```

```BoolToTextWrappingConverter.cs
[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](https://raw.githubusercontent.com/soi013/CodeSnippetsForCSharp/master/CodeSnippets/GenericConverter.snippet)

ショートカットは`gconv`です。
変換元と変換先の型を入力すると自動でConverterクラスのスケルトンを作成します。
クラス名も `ソース型Toターゲット型Converter`になります。
ついでによく使う名前空間（`System.Windows`等）をUsingに追加します。

```xml:GenericConverter.snippet
<?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>

```

コードスニペットの使用動画
こんな風にリズムよくコーディングができて楽しい！

![GenericConverterTest-2018_03_15-23_32_23-コピー_edit.gif](https://qiita-image-store.s3.amazonaws.com/0/120077/e52a9d45-3e9e-c79f-c105-61932df8c36c.gif)



# 環境
VisualStudio2017
.NET Framework 4.7
C#7.1