次の方法で共有


MVVMパターンを使った際のウィンドウ間でのデータの受渡し

質問

2012年12月5日水曜日 5:03

いつもお世話になっております。

表題の通りなのですが、MVVMを意識した場合どうやって作ったら良いか分かりません。
ヒントだけでもいただければと思って投稿しました。
簡潔に申しますと受け取り側の場所が分かりません。。。

シチュエーションは、ごくシンプルな感じで考えてます.
メインウィンドウと、選択画面の2つがあるだけです。
仮に選択画面には以下のように書籍を選択するものとします。
__________________________
|  ・吾輩は猫である                |
|  ・羅生門                          |
|  ・ノルウェーの森                           |
|_________________________|

流れも以下のようにごく一般的な感じで考えてます。
1.メインウィンドウから選択画面を起動→
2.選択画面が開く→
3.選択画面で何かしら選択→
4.選択画面が閉じる→
5.メインウィドウに戻る

※選択画面の呼出し自体は出来ています。
以下などを参考にさせていただきました。
http://code.msdn.microsoft.com/windowsdesktop/Behavior-beae13a6

受け取ったデータをそのままメインウィンドウに表示するだけならば、
メインウィドウのXAMLにバインドすれば良いのかな?と思います。

では、受け取るデータが画面に表示する以外の物もある場合、
例えば、選択した書籍の作者名も取得(画面上には表示しないが内部的に保持)する場合
その値はどこで取得するべきでしょうか?

XAML(View)ではないのは分かります。
となるとメインウィドウ側のViewModelになるでしょうか?
あるいはModelでしょうか?

受け取った値をどうするかによって違うものでしょうか?
そうだった場合、
例えば、選択画面が閉じた際、作者名の書籍一覧を表示するとなったら
どこで受け取るべきでしょうか?

すべての返信 (11)

2012年12月5日水曜日 7:31 ✅回答済み | 2 票

ビヘイビアでメインウインドウのDataContext、つまり、メインウインドウのViewModelが手に入りますから、そのプロパティに選択した情報の入っているオブジェクトを渡してあげれば良いと思います。ただし、これだとビヘイビアにそのViewModelへのキャストが必要になりますから、インターフェース化した方が良いでしょう。

ところで何のためにMVVMを行うかを考え、目をつぶるところはつぶるのも良い場合があるのではないかと個人的には思います。何が言いたいかと言いますと、ダイアログ自体はMVVMで作られていても、そのダイアログを呼び出すのは、メインウインドウのViewModelからShowDialogでもいいいんじゃない?ってことです。
ユーザーにとってMVVMで作られているかどうかなど、もちろん関係ないことです。工数や後のメンテナンスなども総合的に考え、何が何でもきっちりとしたMVVMにこだわる必要はないのかな?と思います。Windowsフォームでは普通にShowDialogを使っていたと思いますが、その時のメリット、デメリットを考えてみて下さい。
以上、あくまで私個人の一意見に過ぎませんが。視点が狭くなって本質を忘れることへの警鐘でもあります。

★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/


2012年12月5日水曜日 7:48 ✅回答済み | 1 票

良ければ以下の部分をもう少し詳しく教えていただけないでしょうか?

>「選択された書籍オブジェクト」 をメインウィンドウの ViewModel に渡せばいいのではないかと思います。

少し考えてみました。一部コードビハインドでイベントハンドラも実装してます。あくまで叩き台として考えてみてください。

メインウィンドウ

<Window x:Class="BookSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:BookSample"
        Title="MainWindow" Height="200" Width="300">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Grid>
        <Label Content="{Binding Path=SelectedBook.Name}" />
        <Button Content="選択" Height="24" Name="button1" VerticalAlignment="Bottom" Click="button1_Click" />
    </Grid>
</Window>

メインウィンドウのコードビハインド

using System.Windows;

namespace BookSample {
    public partial class MainWindow : Window {
        MainViewModel _viewModel;

        public MainWindow() {
            InitializeComponent();
            _viewModel = this.DataContext as MainViewModel;
        }

        private void button1_Click(object sender, RoutedEventArgs e) {
            var window = new SelectWindow() { DataContext = new SelectViewModel() };
            window.ShowDialog();
            _viewModel.SelectedBook = window.SelectedBook;
        }
    }
}

メインビューモデル

using System.ComponentModel;

namespace BookSample {
    class MainViewModel : INotifyPropertyChanged {

        Book _Book;
        public Book SelectedBook {
            get { return _Book; }
            set {
                _Book = value;
                this.OnPropertyChanged("SelectedBook");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName) {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

選択画面

<Window x:Class="BookSample.SelectWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:BookSample"
        Title="SelectWindow" Height="200" Width="300" >
    <!--<Window.DataContext>
        <local:SelectViewModel />
    </Window.DataContext>-->
    <Grid>
        <ListBox Margin="0,50,0,0"
                 ItemsSource="{Binding Books}" DisplayMemberPath="Name"
                 SelectedItem="{Binding SelectedBook}"/>
    </Grid>
</Window>

選択画面のコードビハインド

using System.Windows;

namespace BookSample {
    /// <summary>
    /// SelectWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class SelectWindow : Window {

        public SelectWindow() {
            InitializeComponent();
        }

        public Book SelectedBook {
            get {
                if (this.DataContext == null) return null;
                return (this.DataContext as SelectViewModel).SelectedBook;
            }
            private set {}
        }
    }
}

選択画面のビューモデル

using System.Collections.ObjectModel;
using System.ComponentModel;

namespace BookSample {
    class SelectViewModel : INotifyPropertyChanged {

        public SelectViewModel() {
            this.Books = new ObservableCollection<Book>();
            this.Books.Add(new Book() { Id = 1, Name = "吾輩は猫である", Author = "夏目漱石" });
            this.Books.Add(new Book() { Id = 2, Name = "羅生門", Author = "芥川龍之介" });
            this.Books.Add(new Book() { Id = 3, Name = "ノルウェーの森", Author = "村上春樹" });
        }

        Book _Book;
        public Book SelectedBook {
            get { return _Book;  }
            set {
                _Book = value;
                this.OnPropertyChanged("SelectedBook");
            }
        }

        ObservableCollection<Book> _Books;
        public ObservableCollection<Book> Books { 
            get { return _Books; } 
            set {
                _Books = value;
                this.OnPropertyChanged("Books");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName) {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

本クラス

namespace BookSample {
    public class Book {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Author { get; set; }
    }
}

以上、参考までに。

#コレクションをどこで保持するかにより「リーク」が生じそうですが、そこまで検;し切れてません、悪しからず。

ひらぽん http://d.hatena.ne.jp/hilapon/


2012年12月5日水曜日 7:56 ✅回答済み | 1 票

サンプル作ってたら早々に「回答済み」になってしまったんでいらんかなーと思ったのですが(苦笑)
ひらぽんさんが意図されているものとは異なるかもしれませんし、どちらかというとモデリングに関する問題なのでやり方は十人十色であることをご理解の上で参考にして下さい。

<Window x:Class="WpfApplication10.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <TextBlock Grid.Row="0" Grid.Column="0" Text="書籍名:" />
        <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Title}"/>
        
        <Button Grid.Row="2" Grid.Column="1" Content="選択" Click="Button_Click"/>
    </Grid>
</Window>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowViewModel();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Window1 window = new Window1();
            window.ShowDialog();

            if (window.SelectedItem != null) ((MainWindowViewModel)this.DataContext).Book = window.SelectedItem;
        }
    }

    class MainWindowViewModel : ViewModelBase
    {
        Book _book;

        public Book Book
        {
            get { return _book; }
            set
            {
                _book = value;
                OnPropertyChanged("Book");
                OnPropertyChanged("Title");
            }
        }

        public string Title
        {
            get 
            { 
                return _book != null ? _book.Title : string.Empty; 
            }
            set 
            {
                if (_book != null)
                {
                    _book.Title = value; 
                    OnPropertyChanged("Title");
                }
            }
        }
    }
<Window x:Class="WpfApplication10.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <Style TargetType="ListBoxItem">
            <EventSetter Event="MouseDoubleClick" Handler="ListBoxItem_MouseDoubleClick" />
        </Style>
    </Window.Resources>
    
    <Grid>
        <ListBox ItemsSource="{Binding Path=Books}" DisplayMemberPath="Title"/>
    </Grid>
</Window>
    public partial class Window1 : Window
    {
        public Book SelectedItem { get; set; }

        public Window1()
        {
            InitializeComponent();
            this.DataContext = new Window1ViewModel();
        }

        private void ListBoxItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            this.SelectedItem = ((ListBoxItem)sender).DataContext as Book;
            this.Close();
        }
    }

    class Window1ViewModel:ViewModelBase
    {
        public List<Book> Books { get; set; }

        public Window1ViewModel()
        {
            this.Books = new List<Book>();

            Author Souseki = new Author { Name = "夏目漱石" };
            Souseki.Books.Add(new Book { Title = "吾輩は猫である", Author = Souseki });
            Souseki.Books.Add(new Book { Title = "坊ちゃん", Author = Souseki });
            this.Books.AddRange(Souseki.Books);

            Author Akutagawa = new Author { Name = "芥川龍之介" };
            Akutagawa.Books.Add(new Book { Title = "羅生門", Author = Akutagawa });
            Akutagawa.Books.Add(new Book { Title = "地獄変", Author = Akutagawa });
            this.Books.AddRange(Akutagawa.Books);

            Author Haruki = new Author { Name = "村上春樹" };
            Haruki.Books.Add(new Book { Title = "ノルウェイの森", Author = Haruki });
            Haruki.Books.Add(new Book { Title = "1Q84", Author = Haruki });
            this.Books.AddRange(Haruki.Books);
        }
    }
    class ViewModelBase : INotifyPropertyChanged
    {
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class Book
    {
        public string Title { get; set; }
        public Author Author { get; set; }
    }

    public class Author
    {
        public string Name { get; set; }
        public List<Book> Books { get; set; }

        public Author()
        {
            this.Books = new List<Book>();
        }
    }

で、その上で

>> では、受け取るデータが画面に表示する以外の物もある場合、
>> 例えば、選択した書籍の作者名も取得(画面上には表示しないが内部的に保持)する場合
>> その値はどこで取得するべきでしょうか?
>> XAML(View)ではないのは分かります。
>> となるとメインウィドウ側のViewModelになるでしょうか?
>> あるいはModelでしょうか?

あくまでもモデルの関連性次第なのですが、私は上記のようにModel側に持たせました。これはBookとAuthorの関連性を表現するためです。
無論、この作者名が単なる文字列データとしてのみ扱われるのであれば、Bookの1プロパティになるでしょうし、モデル上で関連性を持たせるほど結合度が低いものであれば、MainViewModel側に別途Authorを用意するでしょうね。
ここらへんはtrapemiyaさんの仰るとおり、そこまで固執すべきではないと私も思います。

以上、参考になれば幸いです。


2012年12月5日水曜日 10:48 ✅回答済み | 1 票

メインウィンドウのViewModelが選択用のViewModelを公開してやると、データのやり取りが楽。
選択結果の通知はViewModel内で処理し、Viewは関知しない。
どのように表示するかはViewにお任せで、選択方法が別ウィンドウだろうがMainWindow内のListBoxだろうがViewModelは関知しない。
デメリットは選択;用のViewModelの寿命をどう扱うか決めないといけないこと。

<!-- MainWindow --><StackPanel>    <StackPanel.DataContext>        <app:MainVM xmlns:app="clr-namespace:WpfApplication1"/>    </StackPanel.DataContext>    <RadioButton Content="シンプル" x:Name="opSimple"/>    <RadioButton Content="詳細" x:Name="opDetail"/>    <Button Content="選択"            Click="button1_Click"             CommandParameter="{Binding Selector}"/>    <TextBlock  Text="{Binding Path=SelectedBook.Name}" />    <TextBlock  Text="{Binding Path=SelectedBook.Author}" /></StackPanel>

<!-- SimpleSelectorWindow --><ListBox ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}" DisplayMemberPath="Name"/>

<!-- DetailSelectorWindow --><ListBox ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}" >    <ListBox.ItemTemplate>        <DataTemplate>            <StackPanel Orientation="Horizontal">                <TextBlock Text="{Binding Path=Name}" />                <TextBlock Text="{Binding Path=Author}" Margin="20,0,0,0"/>            </StackPanel>        </DataTemplate>    </ListBox.ItemTemplate></ListBox>
public partial class MainWindow : Window{    public MainWindow()    {        InitializeComponent();    }    private void button1_Click(object sender, RoutedEventArgs e)    {        Window wnd;        if(opSimple.IsChecked ?? false )            wnd = new SimpleSelectorWindow();        else            wnd = new DetailSelectorWindow ();        wnd.DataContext = ((ICommandSource)sender).CommandParameter;        wnd.Owner = this;        wnd.ShowDialog();    }}public class VMBase : INotifyPropertyChanged{    public event PropertyChangedEventHandler PropertyChanged;    protected virtual void OnPropertyChanged(string name)    {        if (PropertyChanged != null)            PropertyChanged(this, new PropertyChangedEventArgs(name));    }}public class MainVM : VMBase{    public Book SelectedBook    {        get { return _SelectedBook; }        set        {            if (_SelectedBook != value)            {                _SelectedBook = value;                OnPropertyChanged("SelectedBook");            }        }    }    private Book _SelectedBook;    public SelectorVM Selector    {        get        {            if (_Selector == null)            {                _Selector = new SelectorVM(this);            }            return _Selector;        }    }    private SelectorVM _Selector;}public class SelectorVM : VMBase{    public SelectorVM(MainVM main)    {        this.mainVM = main;    }    private MainVM mainVM;    //BindingのPathでMainVM.SelectedBookってやってもいいんだけどね    public Book SelectedBook    {        get        {            return mainVM.SelectedBook;        }        set        {            mainVM.SelectedBook = value;        }    }    public System.Collections.ObjectModel.ObservableCollection<Book> Books    {        get        {            if (_Books == null)            {                _Books = new System.Collections.ObjectModel.ObservableCollection<Book>();                Load();            }            return _Books;        }    }    private System.Collections.ObjectModel.ObservableCollection<Book> _Books;    public void Load()    {        this.Books.Clear();        this.Books.Add(new Book() { Id = 1, Name = "吾輩は猫である", Author = "夏目漱石" });        this.Books.Add(new Book() { Id = 2, Name = "羅生門", Author = "芥川龍之介" });        this.Books.Add(new Book() { Id = 3, Name = "ノルウェーの森", Author = "村上春樹" });    }    public bool Opened    {        get        {            return _Opened;        }        set        {            if (_Opened != value)            {                if (!value)                {                    Books.Clear();                }                _Opened = value;                OnPropertyChanged("Opened");            }        }    }    private bool _Opened;}public class Book{    public int Id { get; set; }    public string Name { get; set; }    public string Author { get; set; }}

#寿命管理を気にしだすと面倒になって、結局はViewModelからShowDialogするのに落ち着いたりしますが。

個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)


2012年12月5日水曜日 5:52 | 1 票

では、受け取るデータが画面に表示する以外の物もある場合、
例えば、選択した書籍の作者名も取得(画面上には表示しないが内部的に保持)する場合
その値はどこで取得するべきでしょうか?

選択画面の ViewModel  に「書籍オブジェクトのコレクション」 をプロパティとして用意し、一覧表示するコントロールにバインド、書籍名のみコントロールに表示すればいいのではないでしょうか?

XAML(View)ではないのは分かります。
となるとメインウィドウ側のViewModelになるでしょうか?
あるいはModelでしょうか?

「選択された書籍オブジェクト」 をメインウィンドウの ViewModel に渡せばいいのではないかと思います。

ひらぽん http://d.hatena.ne.jp/hilapon/


2012年12月5日水曜日 6:56

ひらぽんさま

有難う御座います。

>選択画面の ViewModel  に「書籍オブジェクトのコレクション」 をプロパティとして用意し、一覧表示するコントロールにバインド、書籍名のみコントロールに表示すればいいのではないでしょうか?

なるほど!

>「選択された書籍オブジェクト」 をメインウィンドウの ViewModel に渡せばいいのではないかと思います。

やはりそうですよね。

まだ時間かかりますが早速実装してみます。

有難う御座いました!


2012年12月5日水曜日 7:20

ひらぽんさま

すみません。

具体的な実装が分からないため、

「回答としてマーク」をいったん外させていただきました(T0T)

良ければ以下の部分をもう少し詳しく教えていただけないでしょうか?

>「選択された書籍オブジェクト」 をメインウィンドウの ViewModel に渡せばいいのではないかと思います。


2012年12月6日木曜日 8:50

trapemiya様

有難う御座います。

>ビヘイビアでメインウインドウのDataContext、つまり、メインウインドウのViewModelが手に入りますから、

最初、私の知識では文章だけでは何となくの理解でしたが、
ひらぽん様がご提示くださったサンプルのまさしくこの部分(メインウィンドウのコンストラクタ)のことで良いですよね?

public MainWindow() {
        InitializeComponent();
       _viewModel = this.DataContext as MainViewModel;
}

>ダイアログ自体はMVVMで作られていても、そのダイアログを呼び出すのは、メインウインドウのViewModelからShowDialogでもいいいんじゃない?ってことです。
なるほど!と思いました。

>ユーザーにとってMVVMで作られているかどうかなど、もちろん関係ないことです。工数や後のメンテナンスなども総合的に考え、何が何でもきっちりとしたMVVMにこだわる必要はないのかな?と思います。
おっしゃる通りですね。
今回時間に余裕があるのと、MVVMが初学のためここに焦点を当てておりました。。。

>Windowsフォームでは普通にShowDialogを使っていたと思いますが
はい。そうです。

>視点が狭くなって本質を忘れることへの警鐘でもあります。
おっしゃる通りです。
今回は勉強メインでしたのでこだわっていましたが、今後は柔軟な視点・考えでいきたいと思いました。
ご親切なお言葉、本当に有難う御座います。
今後の指針になりました。


2012年12月6日木曜日 8:52

ひらぽんさま

ソースまでいただき本当に有難う御座います。

お返事遅くなり申し訳ありません。
まだ全部理解出来ていませんがソースの解析に一日ほどかかりました。。

>#コレクションをどこで保持するかにより「リーク」が生じそうですが、そこまで検;し切れてません、悪しからず。
注意点まで有難う御座います。

ところでメインウィンドウのコードビハインドで選択画面の起動時に
ViewModelを渡すようにしているのは何故でしょうか?

 var window = new SelectWindow() { DataContext = new SelectViewModel() };

SelectWindowのXAML側にも以下のコメントがありました。
<!--<Window.DataContext>
       <local:SelectViewModel />
</Window.DataContext>-->

XAML側でなく呼び出し時に渡すようにしたメリットというのが私には思いつきませんでした。
良ければ最後にこれだけ教えていただけないでしょうか?


2012年12月6日木曜日 8:53

みっと様

有難う御座います。
お返事遅くなり申し訳ありません。
まだ全部理解出来ていませんがソースの解析に一日ほどかかりました。。

>サンプル作ってたら早々に「回答済み」になってしまったんでいらんかなーと思ったのですが(苦笑)
すみません!
浅い理解だけで回答済みにして、実際に実装しようとしたら???となりました(ToT)!
さらに、このテーマはけっこう模索されている方は自分以外にも多いんじゃないかと思いまして。。。
出来れば指針的なサンプルをいただきたいなーと・・・(^_^;)

>どちらかというとモデリングに関する問題なのでやり方は十人十色であることをご理解の上で参考にして下さい。
なるほど。そうですよね。

>あくまでもモデルの関連性次第なのですが、私は上記のようにModel側に持たせました。これはBookとAuthorの関連性を表現するためです。
なるほど。参考になります。

大変参考になりました。
本当に有難う御座います!


2012年12月6日木曜日 8:54

gekka様

有難う御座います。
お返事遅くなり申し訳ありません。
まだ全部理解出来ていませんがソースの解析に一日ほどかかりました。。

SimpleSelectorWindowとDetailSelectorWindowを仮に作って試させていただきました。
こういった方法もあるんですね。
今のところ自分の能力では全ては把握出来ていません。
これからじっくり参考にさせていただきます。

>#寿命管理を気にしだすと面倒になって、結局はViewModelからShowDialogするのに落ち着いたりしますが。
寿命の管理など別の問題が出てくるわけですね。参考になります。