多数の値変換を必要とするビューを持つWPFアプリケーションに取り組んでいます。当初、私の哲学(一部は XAML弟子に関する活発な議論 に触発されました)は、ビューモデルをdata要件のサポートについて厳密に作成する必要があるというものでした景色。つまり、データを可視性、ブラシ、サイズなどに変換するために必要な値の変換は、値コンバーターと多値コンバーターで処理されます。概念的には、これは非常にエレガントに見えました。ビューモデルとビューの両方に明確な目的があり、うまく分離されます。 「データ」と「ルック」の間に明確な線が引かれます。
さて、この戦略に「古い大学の試み」を与えた後、私はこの方法を開発し続けたいかどうか疑問に思っています。私は実際に値コンバーターをダンプし、(ほとんど)すべての値変換の責任をビューモデルの手に委ねることを強く検討しています。
値コンバーターを使用するという現実は、明確に分離された懸念の見かけの価値を十分に満たしていないようです。値コンバーターに関する私の最大の問題は、それらを使用するのが面倒なことです。新しいクラスを作成し、IValueConverter
またはIMultiValueConverter
を実装し、object
の値を正しい型にキャストし、_DependencyProperty.Unset
_をテストする必要があります(少なくとも多値コンバータの場合)、変換ロジックを記述し、 コンバーターをリソースディクショナリに登録する [以下の更新を参照]、そして最後に、かなり冗長なXAMLを使用してコンバーターをフックします(両方のバインディングにマジックストリングを使用する必要があります) コンバーターの名前 [以下の更新を参照])。特にVisual Studioのデザインモード/ Expression Blendでは、エラーメッセージがわかりにくいことが多いため、デバッグプロセスもピクニックではありません。
これは、ビューモデルをすべての値の変換に対応させるという代替策が改善であると言っているのではありません。これは、反対側の芝生がより緑になっている問題である可能性が非常に高いです。懸念の優雅な分離を失うことに加えて、派生プロパティの束を記述し、基本プロパティを設定するときにRaisePropertyChanged(() => DerivedProperty)
を慎重に呼び出す必要があります。これは、不愉快なメンテナンスの問題である可能性があります。
以下は、ビューモデルが変換ロジックを処理できるようにし、値コンバーターを廃止することの賛否両論をまとめた最初のリストです。
*取り消し線は、マークアップ拡張機能を使用すると消える利点を示します(下記の更新を参照)
RaisePropertyChanged
を呼び出す必要がありますだから、おそらくあなたが言うことができるように、私はこの問題についていくらか胸を痛めている。値コンバーターを使用する場合でも、ビューモデルで多数の値変換プロパティを公開する場合でも、コーディングプロセスが非効率的で面倒であることに気づくためだけに、リファクタリングの道を進むのは非常にためらっています。
長所/短所が不足していますか?値変換の両方の手段を試した人にとって、あなたにとってどちらがより効果的だったと思いますか、そしてそれはなぜですか?他の選択肢はありますか? (弟子たちは型記述子プロバイダーについて何か言及しましたが、私は彼らが話していることを理解することができませんでした。これについての洞察はありがたいです。)
更新
値コンバーターを登録する必要をなくすために、「マークアップ拡張」と呼ばれるものを使用することが可能であることを今日知りました。実際、これらを登録する必要がなくなるだけでなく、_Converter=
_と入力したときにコンバーターを選択するためのインテリセンスが提供されます。ここに私を始めた記事があります: http://www.wpftutorial.net/ValueConverters.html 。
マークアップ拡張機能を使用する機能により、上記の長所と短所のリストとディスカッションのバランスが多少変更されます(取り消し線を参照)。
この啓示の結果として、私はBoolToVisibility
のコンバーターと私がMatchToVisibility
と呼ぶものと他のすべての変換のビューモデルを使用するハイブリッドシステムを実験しています。 MatchToVisibilityは基本的に、バインドされた値(通常は列挙型)がXAMLで指定された1つ以上の値と一致するかどうかを確認できるコンバーターです。
例:
_Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue=Visible, IfFalse=Hidden, Value1=Finished, Value2=Canceled}}"
_
基本的にこれは、ステータスが完了またはキャンセルのいずれであるかをチェックします。表示されている場合、表示設定は「表示」に設定されます。それ以外の場合は、「非表示」に設定されます。これは非常に一般的なシナリオであることがわかり、このコンバーターを使用すると、ビューモデルの約15のプロパティ(および関連するRaisePropertyChangedステートメント)を節約できました。 _Converter={vc:
_を入力すると、「MatchToVisibility」がインテリセンスメニューに表示されることに注意してください。これにより、エラーの可能性が大幅に減少し、値コンバーターの使用が面倒になります(必要な値コンバーターの名前を覚えたり調べたりする必要がありません)。
気になる方のために、以下のコードを貼り付けます。このMatchToVisibility
の実装の重要な機能の1つは、バインドされた値がenum
であるかどうかを確認し、そうである場合は_Value1
_、_Value2
_なども同じ型の列挙型です。これにより、列挙値のいずれかが誤って入力されているかどうかの設計時および実行時のチェックが提供されます。これをコンパイル時のチェックに改善するには、代わりに次のコマンドを使用できます(これは手で入力したので、間違えた場合は許してください)。
_Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue={x:Type {win:Visibility.Visible}},
IfFalse={x:Type {win:Visibility.Hidden}},
Value1={x:Type {enum:Status.Finished}},
Value2={x:Type {enum:Status.Canceled}}"
_
これはより安全ですが、私にとって価値があるほど冗長ではありません。これを行う場合は、ビューモデルのプロパティを使用することもできます。とにかく、これまでに試したシナリオでは、設計時チェックが完全に適切であることがわかりました。
これがMatchToVisibility
のコードです
_[ValueConversion(typeof(object), typeof(Visibility))]
public class MatchToVisibility : BaseValueConverter
{
[ConstructorArgument("ifTrue")]
public object IfTrue { get; set; }
[ConstructorArgument("ifFalse")]
public object IfFalse { get; set; }
[ConstructorArgument("value1")]
public object Value1 { get; set; }
[ConstructorArgument("value2")]
public object Value2 { get; set; }
[ConstructorArgument("value3")]
public object Value3 { get; set; }
[ConstructorArgument("value4")]
public object Value4 { get; set; }
[ConstructorArgument("value5")]
public object Value5 { get; set; }
public MatchToVisibility() { }
public MatchToVisibility(
object ifTrue, object ifFalse,
object value1, object value2 = null, object value3 = null,
object value4 = null, object value5 = null)
{
IfTrue = ifTrue;
IfFalse = ifFalse;
Value1 = value1;
Value2 = value2;
Value3 = value3;
Value4 = value4;
Value5 = value5;
}
public override object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
var ifTrue = IfTrue.ToString().ToEnum<Visibility>();
var ifFalse = IfFalse.ToString().ToEnum<Visibility>();
var values = new[] { Value1, Value2, Value3, Value4, Value5 };
var valueStrings = values.Cast<string>();
bool isMatch;
if (Enum.IsDefined(value.GetType(), value))
{
var valueEnums = valueStrings.Select(vs => vs == null ? null : Enum.Parse(value.GetType(), vs));
isMatch = valueEnums.ToList().Contains(value);
}
else
isMatch = valueStrings.Contains(value.ToString());
return isMatch ? ifTrue : ifFalse;
}
}
_
これがBaseValueConverter
のコードです
_// this is how the markup extension capability gets wired up
public abstract class BaseValueConverter : MarkupExtension, IValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
public abstract object Convert(
object value, Type targetType, object parameter, CultureInfo culture);
public virtual object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
_
ToEnum拡張メソッドは次のとおりです
_public static TEnum ToEnum<TEnum>(this string text)
{
return (TEnum)Enum.Parse(typeof(TEnum), text);
}
_
更新2
この質問を投稿してから、「ILウィービング」を使用してプロパティと依存プロパティにNotifyPropertyChangedコードを挿入するオープンソースプロジェクトに出くわしました。これにより、「ステロイドの値コンバーター」としてのビューモデルのジョシュスミスのビジョンの実装は、絶対に簡単になります。 「自動実装プロパティ」を使用するだけで、あとはウィーバーが行います。
例:
このコードを入力した場合:
_public string GivenName { get; set; }
public string FamilyName { get; set; }
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
_
...これはコンパイルされるものです:
_string givenNames;
public string GivenNames
{
get { return givenName; }
set
{
if (value != givenName)
{
givenNames = value;
OnPropertyChanged("GivenName");
OnPropertyChanged("FullName");
}
}
}
string familyName;
public string FamilyName
{
get { return familyName; }
set
{
if (value != familyName)
{
familyName = value;
OnPropertyChanged("FamilyName");
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
_
これにより、入力、読み取り、過去のスクロールなどを行う必要があるコードの量が大幅に節約されます。ただし、より重要なことに、依存関係を把握する必要がなくなります。 FullName
のような新しい「プロパティ取得」を追加できます。依存関係のチェーンを入念に調べてRaisePropertyChanged()
呼び出しを追加する必要はありません。
このオープンソースプロジェクトは何と呼ばれていますか?元のバージョンは「NotifyPropertyWeaver」と呼ばれていますが、所有者(Simon Potter)は、一連のILウィーバー全体をホストするための「Fody」と呼ばれるプラットフォームを作成しています。この新しいプラットフォームでのNotifyPropertyWeaverに相当するものは、PropertyChanged.Fodyと呼ばれます。
NotifyPropertyWeaver(インストールは少し簡単ですが、将来バグ修正以降に更新されるとは限りません)を使用したい場合、プロジェクトサイトは次のとおりです http://code.google。 com/p/notifypropertyweaver /
いずれにせよ、これらのILウィーバーソリューションは、ステロイドと値コンバーターのビューモデル間の議論における計算を完全に変更します。
場合によってはValueConverters
を使用し、ロジックをViewModel
に配置することもあります。私の感じでは、ValueConverter
はView
レイヤーの一部になるので、ロジックが実際にView
の一部である場合はそこに配置し、そうでない場合はViewModel
に配置します。
個人的には、ViewModel
esのようなView
固有の概念を扱うBrush
の問題は発生しません。私のアプリケーションでは、ViewModel
は、View
のテスト可能でバインド可能なサーフェスとしてのみ存在するためです。ただし、一部の人々はViewModel
に多くのビジネスロジックを配置し(私はしません)、その場合、ViewModel
はビジネスレイヤーの一部に似ているため、その場合、WPF固有のものは必要ありません。
私は別の分離を好む:
View
-WPFのもの、時にはテストできない(XAMLや分離コードなど)がValueConverter
sViewModel
-WPF固有のテスト可能でバインド可能なクラスEditModel
-操作中にモデルを表すビジネスレイヤーの一部EntityModel
-モデルを永続化して表すビジネスレイヤーの一部Repository
-データベースへのEntityModel
の永続化を担当しますだから、私がやる方法では、ValueConverter
sはほとんど使いません
私があなたの「Con」のいくつかから離れた方法は、私のViewModel
を非常に一般的なものにすることです。たとえば、私が持っているViewModel
というChangeValueViewModel
は、LabelプロパティとValueプロパティを実装しています。 View
には、LabelプロパティにバインドするLabel
と、ValueプロパティにバインドするTextBox
があります。
次に、ChangeValueView
タイプからキー入力されたDataTemplate
であるChangeValueViewModel
を取得します。 WPFがそのViewModel
を検出すると、そのView
が適用されます。私のChangeValueViewModel
のコンストラクターは、EditModel
(通常はFunc<string>
を渡すだけ)から状態を更新するために必要な対話ロジックと、ユーザーが値を編集するときに必要なアクション(一部を実行するAction
のみ)を取りますEditModel
のロジック)。
親ViewModel
(画面の場合)は、コンストラクターでEditModel
を受け取り、ViewModel
などの適切な基本ChangeValueViewModel
sをインスタンス化するだけです。親のViewModel
は、ユーザーが変更を加えたときに実行するアクションを挿入しているため、これらのアクションをすべて傍受して、他のアクションを実行できます。したがって、ChangeValueViewModel
に挿入された編集アクションは次のようになります。
(string newValue) =>
{
editModel.SomeField = newValue;
foreach(var childViewModel in this.childViewModels)
{
childViewModel.RefreshStateFromEditModel();
}
}
明らかにforeach
ループは他の場所でリファクタリングできますが、これはアクションを実行してモデルに適用し、次に(モデルが何らかの未知の方法でその状態を更新したと仮定して)すべての子ViewModel
sに移動して状態を取得するように伝えます再びモデルから。状態が変化した場合、必要に応じて、PropertyChanged
イベントを実行する必要があります。
これにより、たとえばリストボックスと詳細パネルの間のやり取りが非常にうまく処理されます。ユーザーが新しい選択肢を選択すると、EditModel
は選択肢で更新され、EditModel
は詳細パネルに公開されているプロパティの値を変更します。詳細パネル情報の表示を担当するViewModel
の子は、新しい値を確認する必要があることを自動的に通知され、変更されている場合は、PropertyChanged
イベントを発生させます。
オブジェクトの可視性の決定、表示する画像の決定、使用するブラシの色の決定など、変換がビューに関連するものである場合は、常にビューにコンバーターを配置します。
フィールドをマスクする必要があるかどうかを判断するなどのビジネス関連の場合、またはユーザーがアクションを実行する権限を持っている場合は、ViewModelで変換が行われます。
あなたの例から、WPFの大きな部分が欠けていると思います:DataTriggers
。条件値を決定するためにコンバーターを使用しているようですが、コンバーターは実際には1つのデータ型を別のデータ型に変換するためのものです。
上記の例では
例:ラジオ、CD、またはMP3モードのステレオシステムがあるとします。 UIのさまざまな部分の各モードに対応するビジュアルがあると仮定します。 (1)どのグラフィックがどのモードに対応するかをビューに決定させ、それに応じてオン/オフを切り替える、(2)各モード値(IsModeRadio、IsModeCDなど)のビューモデルのプロパティを公開する、または(3)公開する各グラフィック要素/グループのビューモデルのプロパティ(IsRadioLightOn、IsCDButtonGroupOnなど)。 (1)モード認識をすでに持っているので、私の見解には自然に合いました。この場合どう思いますか?
表示する画像を決定するには、DataTrigger
ではなくConverter
を使用します。コンバーターは、あるデータ型を別のデータ型に変換するためのものであり、トリガーは、値に基づいていくつかのプロパティを決定するために使用されます。
<Style x:Key="RadioImageStyle">
<Setter Property="Source" Value="{StaticResource RadioImage}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Mode}" Value="CD">
<Setter Property="Source" Value="{StaticResource CDImage}" />
</DataTrigger>
<DataTrigger Binding="{Binding Mode}" Value="MP3">
<Setter Property="Source" Value="{StaticResource MP3Image}" />
</DataTrigger>
</Style.Triggers>
</Style>
このためにコンバーターの使用を検討するのは、バインドされた値に実際に画像データが含まれていて、UIが理解できるデータ型に変換する必要がある場合のみです。たとえば、データソースにImageFilePath
というプロパティが含まれている場合、コンバーターを使用して、画像ファイルの場所を含む文字列をBitmapImage
に変換することを検討します。私の画像
<Style x:Key="RadioImageStyle">
<Setter Property="Source" Value="{Binding ImageFilePath,
Converter={StaticResource StringPathToBitmapConverter}}" />
</Style>
最終結果として、あるデータ型を別のデータ型に変換する汎用コンバーターでいっぱいの1つのライブラリ名前空間があり、新しいコンバーターをコーディングする必要はほとんどありません。特定の変換用のコンバーターが必要になる場合がありますが、頻度が非常に低いため、それらを記述してもかまいません。
何をテストしているかに依存します。
テストなし:ViewコードとViewModelを自由に組み合わせます(後でいつでもリファクタリングできます)。
ViewModel以下でのテスト:コンバーターを使用します。
モデルレイヤー以下のテスト:自由にViewModelを含むViewコードを混在させる
ViewModelはビューのモデルを抽象化します。個人的には、ブラシなどにViewModelを使用し、コンバーターをスキップします。 Test on the layer(s)where data is in the "purest" form(ie。Model layer) 。
これはおそらくあなたが言及したすべての問題を解決するわけではありませんが、考慮すべき2つの点があります:
まず、最初の戦略のどこかにコンバーターのコードを配置する必要があります。ビューまたはビューモデルのその部分を考慮しますか?ビューの一部である場合は、ビューモデルではなくビュー固有のプロパティをビューに配置しませんか?
第2に、コンバーター以外の設計が、既に存在する実際のオブジェクトプロパティを変更しようとしているように思われます。彼らはすでにINotifyPropertyChangedを実装しているように聞こえるので、ビュー固有のラッパーオブジェクトを作成してバインドしませんか?以下に簡単な例を示します。
public class RealData
{
private bool mIsInteresting;
public bool IsInteresting
{
get { return mIsInteresting; }
set
{
if (mIsInteresting != null)
{
mIsInteresting = value;
RaisePropertyChanged("IsInteresting");
}
}
}
}
public class RealDataView
{
private RealData mRealData;
public RealDataView(RealData data)
{
mRealData = data;
mRealData.PropertyChanged += OnRealDataPropertyChanged;
}
public Visibility IsVisiblyInteresting
{
get { return mRealData.IsInteresting ? Visibility.Visible : Visibility.Hidden; }
set { mRealData.IsInteresting = (value == Visibility.Visible); }
}
private void OnRealDataPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsInteresting")
{
RaisePropertyChanged(this, "IsVisiblyInteresting");
}
}
}
仮想化を利用するために値コンバーターを使用するとよい場合があります。
グリッド内の数十万のセルのビットマスクされたデータを表示しなければならないプロジェクトでのこの例。すべての単一セルのビューモデルでビットマスクをデコードすると、プログラムの読み込みに時間がかかりすぎました。
しかし、単一のセルをデコードする値コンバーターを作成したとき、プログラムはほんの少しの時間でロードされ、ユーザーが特定のセルを見ているときにのみコンバーターが呼び出されるため、同じように応答しました(そして、呼び出す必要があるだけです)ユーザーがグリッド上でビューをシフトするたびに最大30回)。
MVVMがそのソリューションについてどのように不満を言ったかはわかりませんが、ロード時間を95%短縮しました。