LoginSignup
3
6

More than 5 years have passed since last update.

Generic型を使用した汎用Converter(CodeSnippet付き)

Last updated at Posted at 2018-03-16

概要

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

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

実行結果

題材とするデモアプリの実行結果です。
起動時に青い背景のTextBlockに長い文字列があり見切れています。
スクリーンショット 2018-03-15 22.39.06.png

下のIsTrimをチェックすると文字列の見切れている部分が...と表示されます。
スクリーンショット 2018-03-15 22.39.09.png

IsTrimの代わりにその下のIsWrappをチェックすると文字列が折り返して表示されます。
スクリーンショット 2018-03-15 22.39.17.png

デモアプリコード

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

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

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

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
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型になります。

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

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

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

環境

VisualStudio2017
.NET Framework 4.7
C#7.1

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6