🙌

簡単なハンズオンで学ぶWPFのデータバインディングからのMVVM

2023/02/24に公開

はじめに

ListViewを使ってデータのバインディングを学ぶハンズオン形式の記事です。

VisualStudio 2022を使っています。設定は{}のインデントだけ弄っていて他はデフォルトです。
免責事項てきな何か:私はWPFとMVVMとC#は初心者です

ハンズオン

STEP1 挙動の確認

概念とかすっとばして挙動を見てみます。

  1. VisualStudioで新規のプロジェクトを作成します。ハンズオンのためにプロジェクト名はtestとします。
  2. MainWindow.xamlの<Window>要素を以下のコードに置き換えます。
MainWindow.xaml
<ListView x:Name="testListView" ItemsSource="{Binding}">
    <ListView.ItemTemplate>
        <DataTemplate>
	    <TextBlock Text="{Binding Name}"/>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
  1. 次にMainWindowViewModelというC#を新規に作成し以下のコードを貼り付けます。作成するクラスのnamespaceはMainWindow.xamlと同じにしておきましょう。
MainWindowViewModel.cs
namespace test {
    public class MainWindowViewModel {
        public string Name { get; set; }
    }
}
  1. MainWindow.xaml.csファイルに以下のコメント部分を追加します。
MainWindow.xaml.cs
namespace test
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        
        public MainWindow()
        {
            InitializeComponent();
	    // InitializeComponent();の下に
	    // ここから
            var data = new ObservableCollection<MainWindowViewModel>();
            testListView.DataContext = data;

            data.Add(new MainWindowViewModel { Name = "test"});
	    // ここまで追加
        }
    }
}
  1. コピペが済んだら実行します。(ショートカットはF5)

実行すると以下のような出力になります。

MainWindow.xaml.csで設定した"test"が出力されています。

ポイント

  1. C#でやること
    1. プロパティを持ったクラスを作成する
    2. ObservableCollection<作成したプロパティクラス>の形式でインスタンスを作成する
    3. 上のインスタンスをListViewコントロールのDataContextに代入する
    4. 作成したプロパティのクラスをインスタンス化し、上のインスタンスにAddメソッドで追加する
  2. xamlでやること
    1. <ListView>ItemsSource属性を定義し{Binding}を設定する
    2. ListView > ListView.ItemTemplate > DataTemplateを定義する
    3. <DataTemplate>内にコントロールを追加し、<TextBlock>であればText属性に{Binding プロパティ名}と定義する

MainWindow.xamlで指定している{Binding Name}はMainWindowViewModel.csで定義したNameプロパティとリンクしていますね。これでバインディングができていることになります。

ここで重要なのはObservableCollectionのインスタンスをListeViewのDataContextに代入することです。
ListeViewでバインディングできるものはIEnumerableの実装したオブジェクトであればなんでもいいのですが公式ドキュメントの通りとりあえずObservableCollection<T>を使いましょう。

STEP2 複数パターン

次に複数のプロパティと複数のレコードを表示します。STEP1で使ったコードを次の通り修正します。

  1. MainWindowのxamlの<DataTemplate>要素を以下のコードに置き換えます。
MainWindow.xaml
<StackPanel>
    <TextBlock Text="{Binding Name}"/>
    <TextBlock Text="{Binding SummonsVoice}"/>
</StackPanel>
  1. MainWindowViewModel.csのMainWindowViewModelクラスにプロパティを1つ追加します。
MainWindowViewModel.cs
public string SummonsVoice { get; set; }
  1. MainWindow.xaml.csでdata変数へのAddを追加します。
MainWindow.xaml.cs
data.Add(new MainWindowViewModel { Name = "Zorro", SummonsVoice = "威を示せゾロ!" });
data.Add(new MainWindowViewModel { Name = "Milady", SummonsVoice = "惑わせミラディ!" });
  1. 実行します。

実行すると以下のような表示になります。

ポイント

今回の挙動は理解しやすいかと思います。
追加したプロパティをxamlで定義してあげれば、違うプロパティもバインディングできました。
また、data変数にAdd()をすることでListeViewのレコードも増えました。

1点大きく変更した箇所はxamlの<DataTemplate>の要素の直下が<TextBlock>ではなく<StackPanel>になっている点です。
これは<DataTemplate>が要素を1つしか持てないためです。
今回は<StackPanel>を使っているのでListViewのレコード内の文字が垂直に並んで表示されてますが、ここは<Grid>でも<WrapPanel>でも何でもよくて要素が1つのみ定義されている状態であれば平気です。

STEP3 MVVMの思想的なやりかた

どういうことかというとMainWindowViewModel.csにデータの定義・作成を任せようということです。MVVMの設計思想的にはデータの作成追加など画面に表示するデータに関するものは全てViewModelが行います。早速分離していきましょう。

  1. MainWindowのxamlのItemsSource属性値を以下の通り変更します。
MainWindow.xaml
<ListView x:Name="testListView" ItemsSource="{Binding Personas}">
  1. MainWindowViewModel.csのMainWindowViewModelクラスの中身を以下コードに置き換えます。
MainWindowViewModel.cs
public ObservableCollection<Persona> Personas { get; } = new ObservableCollection<Persona>();

public MainWindowViewModel() {
    this.Personas.Add(new Persona { Name = "Zorro", SummonsVoice = "威を示せゾロ!" });
    this.Personas.Add(new Persona { Name = "Milady", SummonsVoice = "惑わせミラディ!" });
}

public class Persona {
    public string Name { get; set; }
    public string SummonsVoice { get; set; }
}
  1. MainWindow.xaml.csでMainWindowのコンストラクタを以下の2行だけに修正します。
MainWindow.xaml.cs
InitializeComponent();
DataContext = new MainWindowViewModel();
  1. 実行します。

構成を変えただけなので実行してもSTEP2のときと表示は変わりません。

ポイント

主な変更点は以下です。

  • testListView.DataContextではなく、DataContextを使用している
  • DataContextにMainWindowViewModelのインスタンスを入れている
  • xamlの{Binding}が{Binding Personas}になっている

最初に疑問に思うのがtestListView.DataContextではない、DataContextは何を指しているか。これはMainWindow.xamlが持つバインディングデータを指しています。
ここでどうやってListeViewにデータをバインディングしているのでしょうか。それはItemsSource="{Biding Personas}"というバインディングのパスで指定によってバインディングしています。
実は{Binding Personas}{Binding Path=Personas}と同じでDataContextに格納されているインスタンスをルートとしてどのデータをバインディングするかという記法なのです。
階層で見たほうがわかりやすいかもしれません。

DataContext にMainWindowViewModelのインスタンスを代入した場合
DataContext
 |- MainWindowViewModel // インスタンス
    |- Personas //ObservableCollection
       |- [0]
           |- Name
	   |- SummonsVoice
       |- [1]
           |- Name
	   |- SummonsVoice
    |- Persona //プロパティクラス
       |- Name
       |- SummonsVoice

VisualStudioのデバッガからMainWindowのDataContextプロパティを見てみると、確かにDataContextでMainWindowViewModel.Personasが参照できていることがわかります。

また、STEP1で説明した以下の点が破られているように見えましたが、

ObservableCollectionのインスタンスをListeViewのDataContextに代入すること

上記の通りバインディングパスの指定で正しくObservableCollectionのインスタンスがバインディングの対象となっているので説明の通りであることがわかります。

これでMainWindow.xaml.csがかなりすっきりし、データの設定もろもろはMainWindowViewModel.csに移りました。

まとめ

ViewModelを分離させておけば、他のPageで同じデータ定義を使いますことできるのでViewModelの作成をしておくのがおすすめです。
MainWindow.xaml.csでは使用するViewModelを自身のDataContextに代入のみに使用し、データを弄くりまわすのはViewModelに任せましょう。

MVVMについて補足

MVVMとはGUIの作成するとき設計思想でModel、View、ViewModelの3種類にわけられます。今回使ったファイルで例えると以下になります。

  • Model : 今回は登場してません。コントロールやViewにあるデータを扱わない処理や、データそのものを取得・準備、ビジネスロジック部分などその他処理をするファイルが該当します。(例えばDBへのアクセスなど)
  • View : MainWindow.xamlで文字通り画面を司ります。
  • ViewModel : MainWindowViewModel.csでViewとModelの橋渡し役です。ここでView用にもしくはModel用にデータ整形したり、コントロールのイベントをCommandを通して実装します。

今回のハンズオンであればViewModelがModelの役割を担っていますが、大規模アプリケーションになってくるとこの分離が活きてくるんだと思います。(GUIアプリケーション開発に携わったことないのでわかりませんが、、)

ここでvisualstudioでWindowやPageを追加したときにセットで作られるMainWindow.xaml.csとかは何?となりますよね。
これはコードビハインドといい、まとめセクションでも述べましたが、このコードビハインドにはxamlを解析するInitializeComponent()のコールやViewとViewModelのバインディングをするのに使用します。ここにコントロールのイベントを実装するのはMVVMの思想的によくないです。

上記もろもろを簡単に実現するフレームワーク(Prism)があるのでそれを使うのが良さそうです。(学習コスト倍増)

あとがき

もっと柔軟にバインディングしたいとか、違うコントロールの場合でやりたいなどあると思いますが、この記事を試した方はすでに足掛かりはできているといいなって思います。

余談ですが今回MVVMを学ぶためにChatGPTを活用しました。回答全てが真っ当なものであるように感じられたので、ChatGPTを正とせずに疑う心を持っておくことが大事そうです。参考セクションにChatGPTに聞いたことを載せておきます。それにしてもAIすごいな。

参考

https://learn.microsoft.com/ja-jp/xamarin/xamarin-forms/enterprise-application-patterns/mvvm
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/data/data-binding-overview?view=netframeworkdesktop-4.8
https://atmarkit.itmedia.co.jp/ait/articles/1011/09/news102_3.html
https://stackoverflow.com/questions/26353919/wpf-listview-binding-itemssource-in-xaml
★オススメ
https://elf-mission.net/programming/wpf/getting-started-2020/step02/
https://prismlibrary.com/docs/commands/commanding.html

ChatGPTに聞いたこと

ChatGPTに「質問したことをQA形式でまとめて」っていったらまとめてくれたのでそれを添付します。やっぱすごい。

  • Q: WPFでのMVVMパターンとは何ですか?

  • A: WPFでのMVVMパターンとは、Model、View、ViewModelの3つの役割に分けてアプリケーションを構成するアーキテクチャパターンのことです。Modelはデータを表現し、ViewはUI要素を表現し、ViewModelはModelとViewの間でデータをやり取りする中間層となります。

  • Q: MVVMパターンを利用することのメリットは何ですか?

  • A: MVVMパターンを利用することで、アプリケーションをより柔軟に、保守性の高いものにすることができます。Model、View、ViewModelを分離することで、コードの再利用性が高まり、テストや保守がしやすくなります。また、Viewに特化したロジックやUIコードをViewModelに移動させることで、Viewのコードをよりシンプルに保ち、より細かい粒度でコントロールすることができます。

  • Q: MVVMパターンでViewModelとViewをバインディングする方法は何ですか?

  • A: MVVMパターンでViewModelとViewをバインディングするには、ViewのDataContextプロパティにViewModelのインスタンスを設定することが必要です。これは、ViewがViewModelを参照できるようにするためです。XAMLでこの設定を行う場合、Viewのルート要素にDataContextプロパティを設定することでViewModelをバインディングすることができます。C#のコードビハインドでこの設定を行う場合、ViewのコードビハインドでDataContextプロパティにViewModelのインスタンスを代入することでバインディングを行うことができます。

  • Q: Viewに対してコードビハインドは必ず1つ以上必要ですか?

  • A: WPFアプリケーションにおいては、Viewに対してコードビハインドは必ず1つ以上必要です。コードビハインドは、Viewの初期化コードを含むため、Viewの正常な動作に必要不可欠なものとなります。ただし、コードビハインドに記述するコード量をできるだけ減らすため、可能な限りViewModelを利用してViewのロジックを実装することが推奨されます。

実際にはもっと色々しつこく聞きましたが、QAを出力している途中でレスポンスが切れました。ちなみに無料プランです。

Discussion