C# + WPFで、Converter + DependencyObject / MuliBinding を使って動的にViewの書式設定をする

引き続き、Microsoft.TeamFoundation.MVVM名前空間を使って作る、WPFアプリの話です。

いつもはXAMLのTextBlock.TextやDataGridTextColumnなどでStringFormatを使い書式設定をしています。

ただ、1つのViewを使ってViewModelによって表示を切り替えた時に動的に書式設定する必要があったことから、その方法を調べてみました。

 
2015/12/10 追記 ここから

公式Blogに以下の記事が掲載されましたので、Microsoft.TeamFoundation.MVVMの使用前に記事を確認してみてください。
Microsoft.TeamFoundation.MVVM 名前空間の利用について - Visual Studio サポート チーム blog - Site Home - MSDN Blogs

2015/12/10 追記 ここまで

 

環境

  • Windows7
  • .NET Framework 4.5
  • 起動時にメニューが出てきて、ボタンを押すと数字があるWindowが表示
    • 数字はTextBlockにて表示
    • 書式設定は、ViewModelの値に対し、3桁ごとのカンマ区切り・符号逆転の指定を行う

 

静的な指定

動的な指定の前に、XAMLへの静的な指定方法をまとめてみます。

StringFormatを使う

StringFormatを使う箇所が少ない場合には、StringFormatを使います。
カスタム数値書式指定文字列

なお、書式でカンマ,を指定する場合は、\でエスケープする必要があります。

<TextBlock Text="{Binding Path=Quantity, StringFormat={}{0:-##\,#;##\,#}}"/>

 

Converterを使う

同じような書式を何度も書く場合や、与える条件により書式を変える場合には、Converterを使うのが楽でした。

 

Converterの実装

System.Windows.Data.IValueConverterインタフェースを実装するConverterを作ります。

今回はTextBlockなので、Convertメソッドだけの実装としましたが、TwoWayとかの場合は、ConvertBackメソッドの実装も必要となります。

public class ParameterConverter : System.Windows.Data.IValueConverter
{
    public object Convert(object value, Type targetType, object isReverse, System.Globalization.CultureInfo info)
    {
        // XAMLのConverterParameterにて、符号を逆転して表示するかを指定すると、第3引数として渡される
        var format = (bool)isReverse ? "-##,#;##,#" : "##,#";
        return string.Format("{0:" + format + "}", value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo info)
    {
        throw new NotImplementedException();
    }
}

 

XAMLの実装

名前空間を追加して、リソースにConverterを追加します。

なお、ConverterParameterにboolを渡せるようにするためのリソースも設定しておきます。

<Window ...
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:DynamicStringFormatMVVM"

    <Window.Resources>
        <local:ParameterConverter x:Key="parameter"/>

        <System:Boolean x:Key="True">True</System:Boolean>
        <System:Boolean x:Key="False">False</System:Boolean>
    </Window.Resources>

 
あとは、TextBlockのConverterとConverterParameterに設定します。例えば、以下では符号を逆転するようにConverterの動作を指定しています。

<TextBlock Text="{Binding Path=Quantity, Converter={StaticResource parameter}, ConverterParameter={StaticResource True}}"/>

 

動的に書式設定をする案

次に動的に指定する方法を考えてみます。

今回はViewModelの値によって切り替えたいため、今回はデータバインディングを使う方法にしました。

まずはCommandParameterへのデータバインディングを調べましたが、無理なようでした。

そのため、他を調べてみたところ、以下の方法がありました。

  • DependencyObjectのConverterを使う方法
  • MuliBindingのConverterを使う方法

そこで、それぞれについて実装してみました。

 

DependencyObjectのConverterを使う方法

DependencyObjectであるConverterをResourceとして用意して、そこにデータバインディングするような感じになります。

この方法については、以下を参考に、いろいろと手を加えてみました。
[Tips] WinRT Converter Parameter Binding - Mim's Blog - Site Home - MSDN Blogs

また、DependencyObjectについては、以下が参考になりました。
[C#][WPF]DependencyObjectって

 

Converterの実装

先ほどのIValueConverterインタフェースに加え、DependencyObjectを継承して作成します。

  • IValueConverterインタフェースの、Convert/ConvertBackメソッドの実装
  • DependencyPropertyであるIsReversePropertyの実装
  • XAMLでデータバインディングし、Convertメソッド内で使用するための、IsReverseプロパティの実装

を行います。

public class DependencyObjectConverter : DependencyObject, IValueConverter
{
    public bool IsReverse
    {
        get
        {
            return (bool)GetValue(IsReverseProperty);
        }
        set
        {
            SetValue(IsReverseProperty, value);
        }
    }

    public static readonly DependencyProperty IsReverseProperty
        = DependencyProperty.Register("IsReverse", typeof(bool), typeof(DependencyObjectConverter));


    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var format = IsReverse ? "-##,#;##,#" : "##,#";
        return string.Format("{0:" + format + "}", value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

 

XAMLの実装

ConverterのIsReverseプロパティにViewModelのプロパティをデータバインディングしたいところですが、そのままでは直接データバインディングできません。

そのため、以下でやった時と同様、FrameworkElementContentControlを使用してDataContextを取得し、IsReverseプロパティにセットします。
C# + WPFで、複数のViewModelで1個のViewを使いまわす - メモ的な思考的な

 
まず、Window.Resourcesとして、FrameworkElementとConverterを用意します。 今回はConverterのリソースを2つ用意して、ViewModelの別々のプロパティ(Normal/Reverse)にデータバインディングします。

<Window.Resources>
    <FrameworkElement x:Key="proxyElement" DataContext="{Binding}"/>

    <local:DependencyObjectConverter 
                    x:Key="DependencyObjectNormal"
                    IsReverse="{Binding Path=DataContext.Normal, Source={StaticResource ResourceKey=proxyElement}}"/>
    <local:DependencyObjectConverter 
                    x:Key="DependencyObjectReverse"
                    IsReverse="{Binding Path=DataContext.Reverse, Source={StaticResource ResourceKey=proxyElement}}"/>
</Window.Resources>

 
次に、Gridなどの下の適当なところにContentControlを用意します。

<ContentControl Visibility="Collapsed" Content="{StaticResource ResourceKey=proxyElement}"/>

 
最後に、TextBlockのConverterとして、Window.Resourcesで設定したものを指定します。

<TextBlock Text="{Binding Path=Quantity, Converter={StaticResource ResourceKey=DependencyObjectReverse}}"/>

 
なお、

  • Microsoft.TeamFoundation.MVVM名前空間
  • 起動時に親画面が出てきて、親画面のボタンを押すと、数字がある子画面が表示される
    • CommandでWindowDisplayService.ShowDialog()を使って、ViewとViewModelを指定する

という前提があるため、XAMLにはDataContextの記述は不要です。

 

注意するところ

前段の終わりで書いた前提のため、今回の場合は特に問題なかったのですが、気になったDependencyObjectの挙動についてメモを残しておきます。

 
子画面のXAMLにDataContextを設定した場合、挙動が少し変わります。実際には、

DataContextあり DataContextなし
1 Convertメソッドが呼ばれる ViewModelのプロパティが呼ばれる
2 ConverterのIsReverseメソッドが呼ばれる Convertメソッドが呼ばれる
3 ViewModelのプロパティが呼ばれる ConverterのIsReverseメソッドが呼ばれる
4 子画面の表示 子画面の表示

となり、DataContextありの場合ではConverterのIsReverseメソッドよりもViewModelのプロパティの方が後に読み込まれました。

その結果、ViewModelのプロパティの値にかかわらず、ConverterのIsReverseプロパティがfalseになり、符号逆転ができませんでした。

試してみたところでは、

  • コードビハインドでDataContextを設定しても、DataContextありのパターンになる
  • 親画面でWindowDisplayService.ShowDialogメソッドで引き渡すViewModelのプロパティに値をセットしても、その設定した値は無視されて、DataContextありのパターンになる

となり、うまく実現できませんでした。

 
そのため、起動時にViewに対するViewModelを動的に変更する場合は気をつけたほうがいいのかもしれません(もしくは自分のコードにバグがあるかもしれません)。

 

MuliBindingのConverterを使う方法

もう一つの方法として、MuliBindingのConverterを使う方法があります。

そこで、以下の記事を参考に実装してみます。

 

Converterの実装

MultiBinding用のConverterを作ります。

内容としては、System.Windows.Data.IMultiValueConverterインタフェースを実装しますが、ほぼIValueConverterインタフェースと同じです。

XAMLで指定したMuliBindingのBinding順に、values配列に設定されてきます。

public class MultiBindingConverter : System.Windows.Data.IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        // MultiBindingを使うと、第1引数で配列として設定した順に渡される
        var isReverse = values[0] == null ? false : (bool)values[0];
        var displayValue = values[1] == null ? 0 : (int)values[1];

        var format = isReverse ? "-##,#;##,#" : "##,#";
        return string.Format("{0:" + format + "}", displayValue);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

 

XAMLの実装

まずはリソースを用意します。

<Window.Resources>
    <local:MultiBindingConverter x:Key="multiBinding"/>
</Window.Resources>

 
次に、TextBlock.TextへMultiBindingのConverterを指定し、ViewModelのプロパティにデータバインディングします。

<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource multiBinding}">
            <Binding Path="Normal"/>
            <Binding Path="Quantity"/>
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

 
先ほどのDependencyObjectと比べると縦の記述量が増えます。

ただ、DataContextをXAMLへ記述しても問題なく動作しますので、XAMLのプレビューが見えやすくなるかもしれません。

 

ソースコード

GitHubに上げました。
CSharp-Sample/MVVMApp/DynamicStringFormatMVVM at master · thinkAmi/CSharp-Sample

 

参考