55

わかりました。専門家のMVVM開発者がWPFでopenfileダイアログをどのように処理するかを本当に知りたいです。

ViewModelでこれを実行したくありません(「Browse」はDelegateCommandを介して参照されます)

void Browse(object param)
{
    //Add code here
    OpenFileDialog d = new OpenFileDialog();

    if (d.ShowDialog() == true)
    {
        //Do stuff
    }
}

それはMVVMの方法論に反すると私は信じているからです。

私は何をしますか?

4

7 に答える 7

37

ここで行う最善のことは、サービスを使用することです。

サービスは、サービスの中央リポジトリ(多くの場合IOCコンテナ)からアクセスする単なるクラスです。次に、サービスはOpenFileDialogなどの必要なものを実装します。

したがって、Unityコンテナ内にあると仮定するとIFileDialogService、次のことができます...

void Browse(object param)
{
    var fileDialogService = container.Resolve<IFileDialogService>();

    string path = fileDialogService.OpenFileDialog();

    if (!string.IsNullOrEmpty(path))
    {
        //Do stuff
    }
}
于 2009-06-25T14:34:54.443 に答える
28

簡単
に言うと、解決策はOpenFileDialog、ビューコンポーネントの一部であるクラスからを表示することです。
つまり、このようなクラスは、ビューモデルに認識されていないクラスである必要があるため、ビューモデルから呼び出すことはできません。
もちろん、ソリューションがMVVMに準拠しているかどうかを評価する場合、コードビハインドは関係ないため、ソリューションにはコードビハインド実装が含まれる可能性があります。

この回答は、元の質問に答えるだけでなく、ビューモデルからダイアログのようなUIコンポーネントを制御すると、MVVMデザインパターンに違反する理由と、ダイアログサービスのような回避策で問題が解決しない一般的な問題についての代替ビューを提供しようとします。

1MVVMとダイアログ

1.1一般的な提案に対する批評

ほとんどすべての答えは、MVVMはパターンであり、クラスレベルの依存関係を対象とし、空の分離コードファイルも必要であるという誤解に従っています。しかし、これはアーキテクチャパターンであり、アプリケーション/コンポーネントレベルで、ビジネスドメインをUIから切り離しておくという別の問題を解決しようとします。
ほとんどの人(ここではSO)は、ビューモデルがダイアログを処理するべきではないことに同意しますが、UI関連のロジックをヘルパークラス(ヘルパーと呼ばれるかサービスと呼ばれるかは関係ありません)に移動することを提案します。モデル。これ(特にサービスバージョン)は、依存関係の非表示
とも呼ばれます。多くのパターンがこれを行います。このようなパターンはアンチパターンと見なされます。Service Locatorは、アンチパターンを隠す最も有名な依存関係です。

そのため、ビューモデルクラスから別のクラスへのUIロジックの抽出を含むパターンもアンチパターンと呼びます。それは元の問題を解決しません:ビューモデル(またはモデル)クラスからUI関連の責任を削除し、それをビュー関連クラスに戻すためにアプリケーション構造またはクラス設計を変更する方法。
言い換えると、重要なロジックは引き続きビューモデルコンポーネントの一部です。

このため、(インターフェイスの背後に隠されているかどうかに関係なく)ダイアログサービスを含む、受け入れられているソリューションのようなソリューションを実装することはお勧めしません。MVVMデザインパターンに準拠するコードを記述したい場合は、ビューモデル内のダイアログビューを処理しないでください。

クラスレベルの依存関係を分離するためのインターフェイス(たとえば、インターフェイス)の導入は、依存性逆転の原則IFileDialogService(SOLIDのD)と呼ばれ、 MVVMとは関係ありません。MVVMとの関連性がない場合、 MVVM関連の問題を解決することはできません。構造物が4階建ての建物であるか超高層ビルであるかにかかわらず、室温に関係がない場合、室温を変更しても、建物が超高層ビルに変わることはありません。MVVMは依存性逆転の同義語ではありません。

MVVMはアーキテクチャパターンですが、依存性逆転はオブジェクト指向言語の原則であり、アプリケーション(別名ソフトウェアアーキテクチャ)の構造化とは何の関係もありません。アプリケーションを構造化するのはインターフェース(または抽象型)ではなく、コンポーネントやモジュールなどの抽象オブジェクトまたはエンティティ(モデル-ビュー-モデルの表示)です。インターフェイスは、コンポーネントまたはモジュールを「物理的に」分離するのに役立つだけです。コンポーネントの関連付けは削除されません。

1.2なぜダイアログや処理Windowは一般的にとても奇妙に感じるのですか?

のようなダイアログコントロールMicrosoft.Win32.OpenFileDialogは「低レベル」のネイティブWindowsコントロールであることに注意する必要があります。MVVM環境にスムーズに統合するために必要なAPIがありません。それらの本質のために、WPFのような高レベルのフレームワークに統合する方法にはいくつかの制限があります。一般に、ダイアログまたはネイティブウィンドウホストは、一般にWPFのような高レベルのフレームワークの既知の「弱点」です。

Windowダイアログは通常、または抽象CommonDialogクラスに基づいています。Windowクラスはであるため、ContentControlスタイルとテンプレートでコンテンツをターゲットにすることができます。
大きな制限の1つは、がWindow常にルート要素でなければならないということです。子としてビジュアルツリーに追加したり、トリガーを使用して表示/起動したり、でホストしたりすることはできませんDataTemplate
の場合、CommonDialog拡張されないため、ビジュアルツリーに追加できませんUIElement

したがってWindow、またはCommonDialogベースの型は常にコードビハインドから表示する必要があります。これが、この種類のコントロールを適切に処理することについて大きな混乱を招く理由だと思います。
さらに、多くの開発者、特にMVVMを初めて使用する初心者は、コードビハインドがMVVMに違反していると認識しています。
いくつかの不合理な理由で、彼らはビューモデルコンポーネントでダイアログビューを処理することはそれほど違反していないことに気づきます。

APIであるため、Window見た目は単純なコントロールのように見えます(実際には拡張されていますContentControl)。しかし、その下では、OSの低レベルに接続します。これを実現するために必要なアンマネージコードはたくさんあります。MFCのような低レベルのC++フレームワークから来ている開発者は、内部で何が起こっているかを正確に知っています。

Windowとクラスはどちらも真のCommonDialogハイブリッドです。これらはWPFフレームワークの一部ですが、ネイティブOSウィンドウのように動作する、または実際にネイティブOSウィンドウになるには、低レベルOSインフラストラクチャの一部でもある必要があります。
WPFWindowCommonDialogクラスは、基本的に複雑な低レベルOSAPIのラッパーです。そのため、このコントロールは、一般的で純粋なフレームワークコントロールと比較すると、(開発者の観点から)奇妙な感じがすることがあります。シンプルなものとして販売されているのはかなり欺瞞的です
。ただし、WPFは高レベルのフレームワークであるため、すべての低レベルの詳細は設計上APIから隠されています。 とに基づいてコントロールを処理する必要があることを受け入れる必要がありますWindowContentControl
WindowCommonDialogC#のみを使用し、そのコードビハインドはデザインパターンにまったく違反しません。

ネイティブのルックアンドフィールと一般的なOS統合を放棄して、テーマやタスクバーなどのネイティブ機能を取得する場合は、関連するプロパティをとして公開するカスタムダイアログを作成するなどして、処理を改善ControlできPopupますDependencyProperty。次に、データバインディングとXAMLトリガーを設定して、通常どおりに可視性を制御できます。

1.3なぜMVVMなのか?

洗練されたデザインパターンやアプリケーション構造がなければ、開発者は、たとえば、データベースデータをテーブルコントロールに直接ロードし、UIロジックとビジネスロジックを組み合わせることになります。このようなシナリオでは、別のデータベースに変更するとUIが壊れます。しかし、さらに悪いことに、UIを変更するには、データベースを処理するロジックを変更する必要があります。また、ロジックを変更する場合は、関連する単体テストも変更する必要があります。

実際のアプリケーションはビジネスロジックであり、派手なGUIではありません。
UIを含めることを強制されることなく、ビジネスロジックの単体テストを作成する必要があります。
ビジネスロジックと単体テストを変更せずにUIを変更したい。

MVVMは、問題を解決し、UIをビジネスロジック、つまりデータをビューから切り離すことができるパターンです。これは、関連するデザインパターンMVCおよびMVPよりも効率的に行われます。

UIをアプリケーションの下位レベルにブリードさせたくありません。データをデータ表示、特にレンダリング(データビュー)から分離したいと考えています。たとえば、データの表示に使用されるライブラリやコントロールを気にすることなく、データベースアクセスを処理したいと考えています。そのため、MVVMを選択します。このため、ビュー以外のコンポーネントにUIロジックを実装することはできません。

1.4名前の付いたクラスから別のクラスにUIロジックを移動してもMVVMViewModelに違反する理由

MVVMを適用することにより、アプリケーションをモデル、ビュー、ビューモデルの3つのコンポーネントに効果的に構造化できます。このパーティショニングまたは構造はクラスに関するものではないことを理解することが非常に重要です。それはアプリケーションコンポーネントについてです。
広く普及しているパターンに従ってクラスに名前を付けたり接尾辞を付けたりすることもできますViewModelが、ビューモデルコンポーネントには通常、名前や接尾辞が付いていないクラスが多数含まれていることを知っておく必要がありViewModelます。ビューモデルは抽象コンポーネントです。

例:
データソースコレクションの作成などの機能をという名前の大きなクラスから抽出し、MainViewModelこの機能をという名前の新しいクラスに移動した場合ItemCreator、このクラスItemCreatorは論理的にはビューモデルコンポーネントの一部です。
クラスレベルでは、機能はMainViewModelクラスの外にあります(MainViewModelコードを呼び出すために、新しいクラスへの強力な参照があります)。アプリケーションレベル(アーキテクチャレベル)では、機能は引き続き同じコンポーネントにあります。

この例を、よく提案されるダイアログサービスに投影できます。ビューモデルからという名前の専用クラスにダイアログロジックを抽出してもDialogService、ロジックはビューモデルコンポーネントの外に移動しません。ビューモデルは、この抽出された機能に依存します。
ビューモデルは引き続きUIロジックに参加します。たとえば、「サービス」を明示的に呼び出して、ダイアログが表示されるタイミングを制御し、ダイアログタイプ自体(ファイルを開く、フォルダの選択、カラーピッカーなど)を制御します。
これにはすべて、UIのビジネスの詳細に関する知識が必要です。定義ごとにビューモデルコンポーネントに属していないという知識。もちろん、そのような知識は、ビューモデルコンポーネントからビューコンポーネントへの結合/依存関係を導入します。

DialogServiceたとえばの代わりにクラスに名前を付けるため、責任は単純に変わりませんDialogViewModel

したがってDialogService、これはアンチパターンであり、UIに依存してUIロジックを実行するビューモデルクラスを実装するという実際の問題を隠します。

1.5コードビハインドの記述はMVVMデザインパターンに違反しますか?

MVVMはデザインパターンであり、デザインパターンは定義ごとにライブラリに依存せず、フレームワークに依存せず、言語またはコンパイラに依存しません。したがって、 MVVMについて話すとき、コードビハインドはトピックではありません。

コードビハインドファイルは、UIコードを記述するための絶対的に有効なコンテキストです。これは、C#コードを含む単なる別のファイルです。コードビハインドとは、「拡張子が.xaml.csのファイル」を意味します。また、イベントハンドラーの唯一の場所でもあります。そして、あなたはイベントから離れたくありません。

「コードビハインドにコードがない」というマントラが存在するのはなぜですか?
WinFormsなどのフレームワークを使用する熟練した経験豊富な開発者など、WPF、UWP、またはXamarinを初めて使用する場合は、UIコードを作成する方法としてXAMLを使用することをお勧めします。の実装StyleまたはDataTemplateC#の使用(コードビハインドファイルなど)は複雑すぎて、非常に読みにくい=>理解しにくい=>保守しにくいコードを生成します。
XAMLは、このようなタスクに最適です。視覚的に冗長なマークアップスタイルは、UIの構造を完全に表現しています。これは、たとえばC#よりもはるかに優れています。XAMLのようなマークアップ言語は、一部の言語より劣っていたり、学ぶ価値がないと感じたりするかもしれませんが、GUIを実装する際の最初の選択肢は間違いありません。XAMLを使用してできるだけ多くのGUIコードを作成するように努める必要があります。

ただし、このような考慮事項は、 MVVMデザインパターンの観点からはまったく関係ありません。

partialコードビハインドは、ディレクティブ(C#)によって実現される単なるコンパイラの概念です。そのため、コードビハインドはデザインパターンとは何の関係もありません。そのため、XAMLもC#もデザインパターンとは何の関係もありません。


2ソリューション

OPのように正しく結論します:

「ViewModel(「ブラウズ」はDelegateCommandを介して参照されます)でこれを[ファイルピッカーダイアログを開く]ことは本当にしたくありません。これはMVVM方法論に反すると信じているためです。

2.1いくつかの基本的な考慮事項

  • ダイアログはUIコントロール、つまりビューです。
  • ダイアログコントロールまたは一般的なコントロールの処理。たとえば、表示/非表示はUIロジックです。
  • MVVM要件:ビューモデルはUIまたはユーザーの存在を認識していません。このため、ビューモデルがユーザー入力をアクティブに待機または呼び出す必要がある制御フローでは、実際に再設計が必要です。これは重大な違反であり、MVVMによって指定されたアーキテクチャの境界を破ります。
  • ダイアログを表示するには、ダイアログを表示するタイミングと閉じるタイミングについての知識が必要です。
  • ダイアログを表示する唯一の理由はユーザーと対話することであるため、ダイアログを表示するにはUIとユーザーについて知る必要があります。
  • ダイアログを表示するには、(適切なダイアログタイプを選択するために)現在のUIコンテキストに関する知識が必要です。
  • MVVMパターンを壊すのは、アセンブリOpenFileDialogやクラスへの依存ではなく、ビューモデルコンポーネントまたはモデルコンポーネントでのUIロジックの実装または参照です(ただし、このような依存関係は貴重なヒントになる可能性があります)。UIElement
  • 同じ理由で、モデルコンポーネントからのダイアログも表示するのは間違いです。
  • UIロジックを担当する唯一のコンポーネントは、ビューコンポーネントです。
  • MVVMの観点からは、C#、XAML、C ++、VB.NETのようなものはありません。partialつまり、関連する悪名高い分離コードファイル(* .xaml.cs)のようなものはありません。コードビハインドの概念は、コンパイラーがクラスのXAML部分をそのC#部分とマージできるようにするために存在します。そのマージ後、両方のファイルは単一のクラスとして扱われます。これは純粋なコンパイラの概念です。partialは、XAMLを使用してクラスコードを記述できるようにする魔法です(実際のコンパイラ言語は、C#またはその他のIL準拠言語のままです)。
  • ICommandは.NETライブラリのインターフェイスであるため、 MVVMについて話すときのトピックではありません。すべてICommandのアクションがビューモデルの実装によってトリガーされる必要があると考えるのは誤りです。コンポーネント間の単方向の依存関係が維持されている限り、イベントはMVVM
    に 準拠する非常に便利な概念です。イベントを使用する代わりに常にの使用を強制すると、OPによって提示されたコードのような不自然で臭いコードにつながります。ICommand
  • ビューモデルクラスによってのみICommand実装されなければならないそのような「ルール」はありません。ビュークラスでも実装できます。 実際、ビューは一般に(または)を実装します。これは両方ともの実装であり、たとえば、またはその他のコントロールからダイアログの表示をトリガーするためにも使用できます。 UIがビューモデルとデータを交換できるようにするデータバインディングがあります(匿名で、データソースの観点から)。ただし、データバインディングでは操作を呼び出すことができないため(少なくともWPFでは-たとえば、UWPではこれが許可されます)、これを実現する必要があります。
    RoutedCommandRoutedUICommandICommandWindow
    ICommandICommandSource
  • 一般に、インターフェイスはMVVMの関連する概念ではありません。したがって、インターフェイス(たとえば)を導入しても、 MVVM関連の問題IFileDialogServiceを解決することはできません。
  • サービスやヘルパークラスはMVVMの概念ではありません。したがって、サービスやヘルパークラスを導入しても、MVVM関連の問題を解決することはできません。
  • クラスとその名前またはタイプ名は、一般的にMVVMの観点からは関係ありません。ビューモデルコードを別のクラスに移動しても、そのクラスに名前が付けられていないか、接尾辞が付いていない場合でも、 MVVM関連の問題ViewModelを解決することはできません。

2.2結論

解決策はOpenFileDialog、ビューコンポーネントの一部であるクラスからを表示することです。
つまり、このようなクラスは、ビューモデルに認識されていないクラスである必要があるため、ビューモデルから呼び出すことはできません。

このロジックは、分離コードファイルまたは他のクラス(ファイル)内に直接実装できます。実装は、単純なヘルパークラスまたはより洗練された(アタッチされた)動作にすることができます。

重要なのは、ダイアログ、つまりUIコンポーネントは、ビューコンポーネントのみで処理する必要があるということです。これは、UI関連のロジックを含む唯一のコンポーネントであるためです。ビューモデルにはビューに関する知識がないため、ビューと通信するために積極的に動作することはできません。パッシブ通信のみが許可されます(データバインディング、イベント)。

ダイアログを使用してユーザーと対話するなどのアクションを実行するために、ビューによって監視できるビューモデルによって発生したイベントを使用して、常に特定のフローを実装できます。

そもそもMVVMに違反しない、ビューモデルファーストアプローチを使用したソリューションが存在します。しかし、それでも、不適切に設計された責任は、このソリューションをアンチパターンに変える可能性もあります。

3特定のダイアログリクエストの必要性を修正する方法

ほとんどの場合、アプリケーションのデザインを修正することで、アプリケーション内からダイアログを表示する必要をなくすことができます。

ダイアログはユーザーとの対話を可能にするUIの概念であるため、UIデザインルールを使用してダイアログを評価する必要があります。
おそらく、UIデザインの最も有名なデザインルールは、90年代にNielsenとMolichによって仮定された10のルールです。

重要なルールの1つは、エラー防止に関するものです。

a)あらゆる種類のエラー、特に入力関連のエラーを防止する必要があり
ます。b)ユーザーは、エラーメッセージやダイアログによって生産性が中断されることを望まないためです。

a)意味:入力データの検証。無効なデータがビジネスロジックに入らないようにしてください。
b)意味:可能な限り、ユーザーにダイアログを表示しないようにします。アプリケーション内からダイアログを表示しないでください。また、マウスクリックなどでユーザーがダイアログを明示的にトリガーできるようにしてください(予期しない中断はありません)。

この単純なルールに従うことで、ビューモデルによってトリガーされるダイアログを表示する必要がなくなります。

ユーザーの観点からは、アプリケーションはブラックボックスです。つまり、データを出力し、データを受け入れ、入力データを処理します。無効なデータを防ぐためにデータ入力を制御する場合、未定義または不正な状態を排除し、データの整合性を確保します。これは、アプリケーション内からユーザーにダイアログを表示する必要がないことを意味します。ユーザーによって明示的にトリガーされたもののみ。

たとえば、一般的なシナリオは、モデルがデータをファイルに永続化する必要があるというものです。宛先ファイルがすでに存在する場合は、このファイルを上書きすることを確認するようにユーザーに依頼します。

エラー防止のルールに従い、常にユーザーが最初にファイルを選択できるようにします。ソースファイルであろうと宛先ファイルであろうと、ファイルダイアログを介して明示的に選択することでこのファイルを指定するのは常にユーザーです。つまり、ユーザーは、たとえば[名前を付けて保存]ボタンをクリックして、ファイル操作を明示的にトリガーする必要もあります。

このように、ファイルピッカーまたはファイル保存ダイアログを使用して、既存のファイルのみが選択されていることを確認できます。ボーナスとして、既存のファイルの上書きについてユーザーに警告する必要がなくなります。

このアプローチに従って、a)「[...]あらゆる種類のエラー、特に入力関連のエラーを防止する」およびb)「[...]ユーザーがエラーメッセージやダイアログによって中断されることを望まない」を満たしました。

アップデート

人々はダイアログビューを処理するためにビューモデルが必要ないという事実に疑問を抱いているので、彼らの主張を証明するためのデータ検証のような余分な「複雑な」要件を考え出すことによって、私はこれらに対処するためのより複雑な例を提供することを余儀なくされています複雑なシナリオ(最初はOPから要求されなかった)。

4例

4.1概要

シナリオは、アルバム名などのユーザー入力を収集し、を使用しOpenFileDialogてアルバム名が保存されている宛先ファイルを選択するための単純な入力フォームです。
3つの簡単な解決策:

解決策1:質問の正確な要件を満たす、非常に単純で基本的なシナリオ。 解決策2:ビューモデルでデータ検証を使用できるようにするソリューション。簡潔にするために、の実装INotifyDataErrorInfoは省略されています。
解決策3ICommand :とを使用ICommandSource.CommandParameterしてダイアログ結果をビューモデルに送信し、操作を実行する、もう1つのより洗練された解決策。

解決策1

OpenFileDialog次の例は、MVVM準拠の方法でを表示するためのシンプルで直感的なソリューションを提供します。
このソリューションにより、ビューモデルはUIコンポーネントやロジックを認識し続けることができます。

FileStreamファイルパスの代わりにビューモデルにを渡すことを検討することもできます。このようにして、必要に応じてダイアログを表示するなど、UIで直接、ストリームを作成しながらエラーを処理できます。

意見

MainWindow.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <StackPanel>
    <!-- The data to persist -->
    <TextBox Text="{Binding AlbumName}" />

    <!-- Show the file dialog. 
         Let user explicitly trigger the file save operation. 
         This button will be disabled until the required input is valid -->
    <Button Content="Save as" 
            Click="AppendAlbumNameToFile_OnClick" />
  </StackPanel>
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();
  }

  private void AppendAlbumNameToFile_OnClick(object sender, EventArgs e)
  {
    var dialog = new OpenFileDialog();

    if (dialog.ShowDialog() == true)
    {
      // Consider to create the FileStream here to handle errors 
      // related to the user's picked file in the view. 
      // If opening the FileStream succeeds, we can pass it over to the viewmodel.
      string destinationFilePath = dialog.FileName;
      (this.DataContext as MainViewModel)?.SaveAlbumName(destinationFilePath);
    }
  }
}

モデルを見る

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged
{
  private string albumName;
  public string AlbumName  
  {
    get => this.albumName;
    set
    {
      this.albumName = value;
      OnPropertyChanged();
    }
  }
    
  // A model class that is responsible to persist and load data
  private DataRepository DataRepository { get; }
    
  // Default constructor
  public MainViewModel() => this.DataRepository = new DataRepository();
    
  // Since 'destinationFilePath' was picked using a file dialog, 
  // this method can't fail. If it fails, the error is not related to the user input
  public void SaveAlbumName(string destinationFilePath)
  {
    // Use a aggregated/composed model class to persist the data
    this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
  }
}

解決策2

より現実的な解決策はTextBox、宛先ファイルのパスを収集するための専用の入力フィールドを追加することです。
ボタンを押すと、オプションのファイルピッカービューを開いて、ユーザーがファイルシステムを参照して宛先パスを探すことができます。

TextBoxブラウザの結果は、ビューモデルクラスにバインドされているに割り当てられます。このようにして、ファイルパスを検証できます。たとえば、次のように実装しますINotifyDataErrorInfo

意見

MainWindow.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <StackPanel>
    <!-- The data to persist -->
    <TextBox Text="{Binding AlbumName}" />

    <!-- Alternative file path input, validated using INotifyDataErrorInfo validation 
         e.g. using File.Exists to validate the file path -->
    <TextBox x:Name="FilePathTextBox" 
             Text="{Binding DestinationPath, ValidatesOnNotifyDataErrors=True}" />

    <!-- Option to search a file using the file picker dialog -->
    <Button Content="Browse" Click="PickFile_OnClick" />

    <!-- Let user explicitly trigger the file save operation. 
         This button will be disabled until the required input is valid -->
    <Button Content="Save as" 
            Command="{Binding SaveAlbumNameCommand}" />
  </StackPanel>
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();
  }

  private void PickFile_OnClick(object sender, EventArgs e)
  {
    var dialog = new OpenFileDialog();

    if (dialog.ShowDialog() == true)
    {
      this.FilePathTextBox.Text = dialog.FileName;

      // Since setting the property explicitly bypasses the data binding, 
      // we must explicitly update it by calling BindingExpression.UpdateSource()
      this.FilePathTextBox
        .GetBindingExpression(TextBox.TextProperty)
        .UpdateSource();
    }
  }
}

モデルを見る

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
  private string albumName;
  public string AlbumName
  {
    get => this.albumName;
    set
    {
      this.albumName = value;
      OnPropertyChanged();
    }
  }

  private string destinationPath;
  public string DestinationPath
  {
    get => this.destinationPath;
    set
    {
      this.destinationPath = value;
      OnPropertyChanged();

      ValidateDestinationFilePath();
    }
  }

  public ICommand SaveAlbumNameCommand => new RelayCommand(
    commandParameter =>
    {
      ExecuteSaveAlbumName(this.TextValue);
    },
    commandParameter => true);

  // A model class that is responsible to persist and load data
  private DataRepository DataRepository { get; }

  // Default constructor
  public MainViewModel() => this.DataRepository = new DataRepository();

  private void ExecuteSaveAlbumName(string destinationFilePath)
  {
    // Use a aggregated/composed model class to persist the data
    this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
  }
}

解決策3

次のソリューションは、2番目のシナリオのより洗練されたバージョンです。プロパティを使用してICommandSource.CommandParameter、ダイアログ結果をビューモデルに送信します(前の例で使用したデータバインディングの代わりに)。

意見

MainWindow.xaml

<Window x:Name="Window">
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <StackPanel>

    <!-- The data to persist -->
    <TextBox Text="{Binding AlbumName}" />

    <!-- Alternative file path input, validated using binding validation 
         e.g. using File.Exists to validate the file path -->
    <TextBox x:Name="FilePathTextBox" 
             Text="{Binding ElementName=Window, Path=DestinationPath, ValidationRules={Binding ElementName=Window, Path=FilePathValidationRules}" />

    <!-- Option to search a file using the file picker dialog -->
    <Button Content="Browse" Click="PickFile_OnClick" />

    <!-- Let user explicitly trigger the file save operation. 
         This button will be disabled until the required input is valid -->
    <Button Content="Save as" 
            CommandParameter="{Binding ElementName=Window, Path=DestinationPath}" 
            Command="{Binding SaveAlbumNameCommand}" />
  </StackPanel>
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public static readonly DependencyProperty DestinationPathProperty = DependencyProperty.Register(
    "DestinationPath",
    typeof(string),
    typeof(MainWindow),
    new PropertyMetadata(default(string)));

  public string DestinationPath
  {
    get => (string)GetValue(MainWindow.DestinationPathProperty);
    set => SetValue(MainWindow.DestinationPathProperty, value);
  }

  public MainWindow()
  {
    InitializeComponent();
  }

  private void PickFile_OnClick(object sender, EventArgs e)
  {
    var dialog = new OpenFileDialog();

    if (dialog.ShowDialog() == true)
    {
      this.DestinationPath = dialog.FileName;
    }
  }
}

モデルを見る

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
  private string albumName;
  public string AlbumName
  {
    get => this.albumName;
    set
    {
      this.albumName = value;
      OnPropertyChanged();
    }
  }

  public ICommand SaveAlbumNameCommand => new RelayCommand(
    commandParameter =>
    {
      ExecuteSaveAlbumName(commandParameter as string);
    },
    commandParameter => true);

  // A model class that is responsible to persist and load data
  private DataRepository DataRepository { get; }

  // Default constructor
  public MainViewModel() => this.DataRepository = new DataRepository();

  private void ExecuteSaveAlbumName(string destinationFilePath)
  {
    // Use a aggregated/composed model class to persist the data
    this.DataRepository.SaveData(this.AlbumName, destinationFilePath);
  }
}
于 2020-11-16T16:24:34.963 に答える
13

答えの1つにコメントしたかったのですが、残念ながら、私の評判はそうするほど高くはありません。

OpenFileDialog()などの呼び出しがあると、ビューモデルのビュー(ダイアログ)が暗示されるため、MVVMパターンに違反します。ビューモデルはGetFileName()のようなものを呼び出すことができます(つまり、単純なバインディングでは不十分な場合)が、ファイル名がどのように取得されるかは気にしないでください。

于 2013-10-17T14:28:49.310 に答える
10

ViewModelはダイアログを開いたり、ダイアログの存在を認識したりしてはなりません。VMが別のDLLに格納されている場合、プロジェクトにPresentationFrameworkへの参照を含めることはできません。

一般的なダイアログのビューでヘルパークラスを使用するのが好きです。

ヘルパークラスは、ウィンドウがXAMLでバインドするコマンド(イベントではない)を公開します。これは、ビュー内でのRelayCommandの使用を意味します。ヘルパークラスはDepencyObjectであるため、ビューモデルにバインドできます。

class DialogHelper : DependencyObject
{
    public ViewModel ViewModel
    {
        get { return (ViewModel)GetValue(ViewModelProperty); }
        set { SetValue(ViewModelProperty, value); }
    }

    public static readonly DependencyProperty ViewModelProperty =
        DependencyProperty.Register("ViewModel", typeof(ViewModel), typeof(DialogHelper),
        new UIPropertyMetadata(new PropertyChangedCallback(ViewModelProperty_Changed)));

    private static void ViewModelProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (ViewModelProperty != null)
        {
            Binding myBinding = new Binding("FileName");
            myBinding.Source = e.NewValue;
            myBinding.Mode = BindingMode.OneWayToSource;
            BindingOperations.SetBinding(d, FileNameProperty, myBinding);
        }
    }

    private string FileName
    {
        get { return (string)GetValue(FileNameProperty); }
        set { SetValue(FileNameProperty, value); }
    }

    private static readonly DependencyProperty FileNameProperty =
        DependencyProperty.Register("FileName", typeof(string), typeof(DialogHelper),
        new UIPropertyMetadata(new PropertyChangedCallback(FileNameProperty_Changed)));

    private static void FileNameProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("DialogHelper.FileName = {0}", e.NewValue);
    }

    public ICommand OpenFile { get; private set; }

    public DialogHelper()
    {
        OpenFile = new RelayCommand(OpenFileAction);
    }

    private void OpenFileAction(object obj)
    {
        OpenFileDialog dlg = new OpenFileDialog();

        if (dlg.ShowDialog() == true)
        {
            FileName = dlg.FileName;
        }
    }
}

ヘルパークラスには、ViewModelインスタンスへの参照が必要です。リソース辞書を参照してください。構築直後に、ViewModelプロパティが設定されます(XAMLの同じ行に)。これは、ヘルパークラスのFileNameプロパティがビューモデルのFileNameプロパティにバインドされている場合です。

<Window x:Class="DialogExperiment.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DialogExperiment"
        xmlns:vm="clr-namespace:DialogExperimentVM;assembly=DialogExperimentVM"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <vm:ViewModel x:Key="viewModel" />
        <local:DialogHelper x:Key="helper" ViewModel="{StaticResource viewModel}"/>
    </Window.Resources>
    <DockPanel DataContext="{StaticResource viewModel}">
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="File">
                <MenuItem Header="Open" Command="{Binding Source={StaticResource helper}, Path=OpenFile}" />
            </MenuItem>
        </Menu>
    </DockPanel>
</Window>
于 2014-04-25T21:20:01.957 に答える
8

たとえば、viewModelのコンストラクターに渡したり、依存性注入を介して解決したりできるサービスを使用しています。例えば

public interface IOpenFileService
{
    string FileName { get; }
    bool OpenFileDialog()
}

そして、内部でOpenFileDialogを使用して、それを実装するクラス。viewModelでは、インターフェイスのみを使用するため、必要に応じてモック/置換できます。

于 2009-06-25T14:37:30.883 に答える
4

私はこの方法でそれを解決しました:

  • ViewModelでインターフェイスを定義し、ViewModelで操作しました
  • ビューでは、このインターフェイスを実装しました。

CommandImplは、以下のコードでは実装されていません。

ViewModel:

namespace ViewModels.Interfaces
{
    using System.Collections.Generic;
    public interface IDialogWindow
    {
        List<string> ExecuteFileDialog(object owner, string extFilter);
    }
}

namespace ViewModels
{
    using ViewModels.Interfaces;
    public class MyViewModel
    {
        public ICommand DoSomeThingCmd { get; } = new CommandImpl((dialogType) =>
        {
            var dlgObj = Activator.CreateInstance(dialogType) as IDialogWindow;
            var fileNames = dlgObj?.ExecuteFileDialog(null, "*.txt");
            //Do something with fileNames..
        });
    }
}

意見:

namespace Views
{
    using ViewModels.Interfaces;
    using Microsoft.Win32;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;

    public class OpenFilesDialog : IDialogWindow
    {
        public List<string> ExecuteFileDialog(object owner, string extFilter)
        {
            var fd = new OpenFileDialog();
            fd.Multiselect = true;
            if (!string.IsNullOrWhiteSpace(extFilter))
            {
                fd.Filter = extFilter;
            }
            fd.ShowDialog(owner as Window);

            return fd.FileNames.ToList();
        }
    }
}

XAML:

<Window xmlns:views="clr-namespace:Views"
        xmlns:viewModels="clr-namespace:ViewModels">    
    <Window.DataContext>
        <viewModels:MyViewModel/>
    </Window.DataContext>
    <Grid>
        <Button Content = "Open files.." Command="{Binding DoSomeThingCmd}"
                CommandParameter="{x:Type views:OpenFilesDialog}"/>
    </Grid>
</Window>
于 2017-05-03T09:28:40.680 に答える
3

サービスを受けることは、viewmodelからビューを開くようなものです。ビューにDependencyプロパティがあり、プロパティの変更時に、FileDialogを開いてパスを読み取り、プロパティを更新して、VMのバインドされたプロパティを更新します

于 2010-03-08T14:17:14.863 に答える