引き続き、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のプロパティをデータバインディングしたいところですが、そのままでは直接データバインディングできません。
そのため、以下でやった時と同様、FrameworkElement
とContentControl
を使用して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を指定する
- Commandで
という前提があるため、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を使う方法があります。
そこで、以下の記事を参考に実装してみます。
- Muhammad Shujaat Siddiqi: WPF - Binding Converter Parameter [Including Discussion about Binding Reflector]
- c# - How to simply bind this to ConverterParameter? - Stack Overflow
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