MVVMでView内のコントロールをViewModelで参照したい
質問
2009年7月15日水曜日 8:17
WPF Model-View-ViewModel Toolkit を使って作ったプロジェクトをベースにアプリケーションを作成しています。
ウィンドウ内の特定のFrameworkElementのスクリーンショットを取るコマンドを作成したいのですが、ViewModelからどうやってView内のコントロールのインスタンスにアクセスすればよいかわかりませんでした。
以下の二つの方法を考えたのですが、うまくいきませんでした
1.コマンドにFrameworkElementを渡すパラメータを付けて、ViewからViewModelに渡す
DelegateCommand に、パラメータを付ける方法がわかりませんでした・・・
2.ViewModelに出力したいFrameWorkElementクラスの参照を格納するプロパティを作成し、Viewのインスタンスで、ViewModelのプロパティに代入する
ViewのDataContextがViewModelなので、↓のようにViewModelのプロパティに代入してやればいいと思ったんですが、ViewにはViewModelのname-spaceが通ってないので、プロパティを参照できませんでした・・・
MyViewModel vm = this.DataContext as MyViewModel;
vm.FileOutFWElement = this.MyCanvas;
作法の問題だとは思うんですけど、こういう場合はどのように記述するのがいいのでしょうか?
よろしくお願いします。
すべての返信 (10)
2009年7月15日水曜日 16:08 ✅回答済み
2.については試したことがあります。
//Viewのコンストラクタ
public PaymentSlipWindow()
{
InitializeComponent();
this.Loaded += this_loaded;
}
//ViewModelのViewプロパティに自分のインスタンス(つまりViewのインスタンス)を渡しています。
private void this_loaded(object sender, RoutedEventArgs e)
{
((IView)this.DataContext).View = this;
}
このことは以下でもちらっと書いています。
MVVMパターン。ViewとViewModelをXAMLだけで結び付けると疎結合が強すぎて使いづらい場合がある。http://blogs.wankuma.com/trapemiya/archive/2009/06/15/175422.aspx
★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://blogs.wankuma.com/trapemiya/
2009年7月15日水曜日 22:56
trapemiya さん。
返信ありがとうございます。
ViewにViewModelのname spaceを通して、アクセス出来るようにしたところ、やりたいことはできるようになりました。ありがとうございました。
using WpfMyApp.ViewModels;
これだと、ViewModelからViewにアクセスし放題ですね。
ViewとViewModelを分離した趣旨をちゃんと理解してないんですけど、とりあえずView丸ごとじゃなくて、必要最小限の情報をViewModelのプロパティに設定するように心がけようと思います。
2009年7月16日木曜日 0:38 | 1 票
これだと、ViewModelからViewにアクセスし放題ですね。
ViewとViewModelを分離した趣旨をちゃんと理解してないんですけど、とりあえずView丸ごとじゃなくて、必要最小限の情報をViewModelのプロパティに設定するように心がけようと思います。
ViewとViewModelを分離する目的は、ViewとViewModelを疎結合にすることです。つまり、ViewはViewModelを知りませんし、ViewModelはViewを知りません。しかし、お互いに他のインスタンスまで知らないようにすることではありません。現にViewはViewModelのインスタンスをDataContextプロパティで取得することができます。そこでViewModelからもViewへのインスタンスを取得できるようにしようという発想が私が上で書いたコードです。
((IView)this.DataContext).View = this;
そうすればNIM5さんが悩まれていたことも解決されるはずです。
そのコードですが、インターフェースを経由することにより、ViewはViewModelと疎結合を保っています。もちろん、インターフェース経由でView自身のインスタンスではなく必要な参照や値のみをViewModelに渡すことでも疎結合は保たれますが、それだと最悪View毎にインターフェースを用意しなければなりません。私が上で書いたコードはどのViewでも汎用的に使えるコードです。こうしておけばViewをBlendなどでデザインした際も、何も考えずにこのコードを書いておけば良いことになります。
また、心配されていらっしゃるようにViewのインスタンス自身を渡してもアクセスし放題にはなりません。クラスのカプセル化という機能があるからです。
上でも述べましたが、ViewからそのDataContextプロパティでViewModelのインスタンスを取得できますが、ViewModelが適切にカプセル化されていれば、アクセスし放題で困るということはありません。 ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://blogs.wankuma.com/trapemiya/
2009年7月16日木曜日 10:57 | 1 票
そうなってるかもしれませんが、ViewModelからコントロールに直接触るのではなく、Viewに、画像を作るロジックを入れて
ViewModelから呼び出す形がいいかな~と思います。
コードイメージは下のような感じです。
public interface IView {
画像データ 画像作る();
}
public class Window1 : Window, IView {
// xamlに DataContextChanged="Window_DataContextChanged"を書いておく
public void Window_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) {
((ViewModel)DataContext).View = this;
}
public class ViewModel {
public IView View { get; set; }
public void 画像保存() {
画像データ data = this.View.画像データ作る();
data.Save(); // なんらかの方法で保存する
}
}
かずき Blog:http://blogs.wankuma.com/kazuki/
2009年7月16日木曜日 15:09 | 1 票
そうなってるかもしれませんが、ViewModelからコントロールに直接触るのではなく、Viewに、画像を作るロジックを入れて
ViewModelから呼び出す形がいいかな~と思います。
このパターンはWindowsフォームで私がよく使います。確かに画像を作るロジックはそうした方が良さそうですね。
私の先のコードを付加すると、私的には次のようなコードイメージになるかな・・・
View.DataContextに対するViewModel.Viewをどうしても作りたいようです。(笑)
public interface IView
{
Window View { get; set; }
}
public interface ICreateImage{
画像データ 画像作る();
}
public class Window1 : Window, ICreateImage{
// xamlに DataContextChanged="Window_DataContextChanged"を書いておく
public void Window_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) {
((IView)DataContext).View = this;
}
public 画像データ 画像作る() {
//画像データを作ります。
}
}
public class ViewModel : IView {
public Window View { get; set; }
public void 画像保存() {
画像データ data = ((ICreateImage)View).画像データ作る();
data.Save(); // なんらかの方法で保存する
}
}
#DataContextChangedで行う方法は勉強になりました。ありがとうございます。
#IViewはジェネリックを使った方がいいな。Window型じゃなくてUserControl型などもありえそうですし。
★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://blogs.wankuma.com/trapemiya/
2009年7月16日木曜日 16:21
trapemiyaさん、かずきさん、返信ありがとうございます。
なるほど・・・インターフェースを使えば、仮にViewにそのインタフェースが無くても例外処理などで対応できるから、疎結合が維持できるってことですね。
ちなみに、私が「一応できた」と言っていたコードは↓のようなものです。
viewに、ViewModelのネームスペースを通し、プロパティ名直打ちで代入してました。
さすがにこれは乱暴だなぁと・・・(笑) インターフェースを使う方法で書き直そうと思います。スクリーンショットを取るメソッドを持つインタフェースは、デバッグも含めていろんなところで使えそうです。
using WpfMyApp.ViewModels;
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
this.Loaded += OnMainViewLoaded;
}
private void OnMainViewLoaded(object sender, RoutedEventArgs e)
{
((MainViewModel)this.DataContext).ScreenShotElement = this.MyCanvas;
}
}
>#IViewはジェネリックを使った方がいいな。Window型じゃなくてUserControl型などもありえそうですし。
WPFに限って言えば、FrameworkElement型にしておけばなんでもいけそうですが、ジェネリックにした方が汎用性は高まるかもしれないですね
2009年7月17日金曜日 0:18
WPFに限って言えば、FrameworkElement型にしておけばなんでもいけそうですが、ジェネリックにした方が汎用性は高まるかもしれないですね
どのみち実際のインターフェース型、もしくは実際のViewの型にキャストする必要があるわけですから、おっしゃる通りFrameworkElement型で十分ですね。フォローありがとうございます。 ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://blogs.wankuma.com/trapemiya/
2009年7月17日金曜日 5:36
1.コマンドにFrameworkElementを渡すパラメータを付けて、ViewからViewModelに渡す
DelegateCommand に、パラメータを付ける方法がわかりませんでした・・・
ICommandインタフェースの CanExecute , Execute を見ると,
Object型 のパラメータを取る形になってますよね。
DelegateCommand の delegateするメソッドが
パラメータ無しのActionデリゲートになっているけれど,
Action<Object> に替えれば,
XAML側で,
Command への Binding に加えて,
CommandParameter に
ElementName や RelativeSource や Source を指定する形で
Binding してやれば,渡って来ます。
cf.
http://code.msdn.microsoft.com/mag200902MVVM/Release/ProjectReleases.aspx?ReleaseId=2026
の MVVM Demo Application の
RelayCommandクラスをそのまま使ったものでは,やれました。
DelegateCommandクラスを詳しく見ていないので分かりませんが,
まるっと入れ替えるか,
RelayCommand というクラスとDelegateCommand クラスを比べて修正すればやれると思います。
稍丼 / yayadon
2009年7月17日金曜日 17:49 | 1 票
DelegateCommand はパラメータをつぶしているので使えません。
パラメータの型を Generic で指定している DelegateCommand<T> を使いましょう。
そうすると何の問題もなく Command に渡せます。
えムナウ@わんくま同盟 Microsoft MVP Visual Studio C# Since 2005/01-2009/12
2009年7月18日土曜日 7:45
(ソースを見ると)
すぐ下に DelegateCommand<T> がありましたね。
稍丼 / yayadon