41
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

WPF超絶入門から学び直し1【MVVM】

Last updated at Posted at 2023-01-11

IT未経験からC#erになり最初の頃だけやっていたWPF。
しばらく別のフレームワークをやっているうちにすべて忘れ去ってしまった。
フレームワーク以前にC#自体の言語仕様で精一杯だったあの頃。覚えてるわけがなかった。

基礎から学びなおす。
だいたい何かしらの記事を読みながらサンプルアプリを再現すると思う。

でもそれだけではまた思い出せなくなってしまうから、この記事に備忘録として自分なりに短いまとめや補足事項、ハマったポイントなどを書き残していく。

初回は超絶基本から。

テーマ
WPFにありがちなMVVMを思い出す。

今回は、WPFを初めて触る人もはや全員お世話になってそうな、こちらの記事を参考にサンプルアプリを作る。
【世界で一番短いサンプルで覚えるMVVM入門】
感想 → 世界一短くてこんなにコード長いのがWPF感。。

:pushpin:補足・まとめ系メモ

View

.xaml.csというやつ。

mainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

Windowsフォームで見たことあるような姿をしている。

.xamlと.xaml.csが対になっている仕組み、あるいは.xaml.csは「コードビハインド」とか呼ばれている。

「コードビハインド(.xaml.cs)にはあまりコードを書くな」とよく言われる。
ここにコードを書きまくると、まるでいにしえのWindowsフォームアプリのようになり、外観と中身をしっかり分別して書けるというWPFの醍醐味を味わえなくなってしまうとのことだ。

ちなみに新生児の頃、.xamlと.xaml.csは必ず対になっているものと誤解していた。
例えば、使いまわしたいXAMLコードはResourceDictionaryという要素で囲って別ファイルにまとめることができる。このようなファイルには対となる.xaml.csは存在しない。参考:C#のWPFでよく使うXAMLの定義を別ファイルにする

InitializeComponentの中も見とく。
ここで対応するxamlのパスを保持して.xamlと自身を紐づけてるようだ。

MainWindow.g.cs
System.Uri resourceLocater = new System.Uri("/WpfSampleApp;component/mainwindow.xaml", System.UriKind.Relative);

バインド

外観と中身の処理をパキッと分離している状態でありながら、どうやって画面に表示する値と処理で使う変数を結びつけているの?それがデータバインディング(バインド)という仕組みだ。

値のバインド

Windowsフォームでは、内部でいろいろと計算処理とかをした後に

Windowsフォーム
textbox1.Text = Val;

的なコードを書くことで、画面の変数に値がセットされて初めて画面が更新される。
この式は、左辺は画面で使う変数、右辺は内部処理で使う変数。そしてこれらをイコールで結んでいるってことは、つまり画面と中身が癒着しちゃってる感じ、ということだね。

あともし更新タイミングが複数箇所あったらその分だけ同じようなコードを書かないと更新できないから、なんか手動って感じもするね。

こういう、画面と中身を結びつける処理をいろんなところに書かないといけないっていうのが気持ち悪い、ということだろうね。初心者のワレはそんなこと思ったことないけどね。ええやん動いたら何でも〜。←

いっぽうWPFでは

WPF(xaml)
<TextBox Text="{Binding Val}"/>

この1つ書いとくだけで画面と中身が連動されるよ。
ただしこれ書いただけでは何も起こらなくて、次に出てくるINotifyProperyChanged関係のこともやらないとだめなんだ。
えぇーめんどくさそー!初心者のワレはそれ理解する暇あったら脳死でWindowsフォームのやり方しときたいよ!って思っちゃうけど設計的には確かにビューリホみあるし分業や保守がしやすい、ということだろうね。

ViewModel

MVVMのViewModelは、ウェッブでよくあるMVCのControllerのようなポジションに思えるがこれとは違う。また、呼称は同じだがMVCのViewModelとも違う。

  • MVCのController…サーバーサイドの受付窓口業務にひたすら専念。処理が少なくてスッキリしている。
  • MVCのViewModel…ViewとControllerの間でやり取りするデータを1つのクラスにまとめたもの。プロパティばかりでメソッドは基本的に持たない、データの入れ物クラス。
  • MVVMのViewModel…Viewにバインドしたい変数やコマンドをプロパティに保持する。それらに付随する処理もいろいろ書くので肥大化しやすい。

以下にViewModelの作り方・使い方をメモする。

①FooViewModel.csは、INotifyPropertyChangedを実装する。

INotifyPropertyChangedのインタフェースメンバ
public event PropertyChangedEventHandler PropertyChanged;

上記インタフェースメンバPropertyChangedは、ViewModelからViewにプロパティの値が変更されたことを通知する役目を持つ。(参考記事は「View Modelに通知する」って書いてあるけど正しくはViewだと思われ)
なのでプロパティのsetアクセサとかにこれを書いてなかったら、中身の変数は更新したのに画面の値は更新されないという現象が起こる。

②バインドするプロパティのsetアクセサから値の変更通知をする。

参考記事では直接PropertyChangedを叩いているが、実務で見た別の方法もメモっとく。

(A) 初心者にも分かりやすいバージョン

FooViewModel.cs (INotifyPropertyChanged実装クラス)
// このメソッドを定義
protected void RaisePropertyChanged(string propertyName)
{
    var h = PropertyChanged;
    if (h != null)
    {
        h(this, new PropertyChangedEventArgs(propertyName));
    }
}

// プロパティのsetアクセサから変更通知
RaisePropertyChanged(nameof(MyProp));

(B) プロパティ名をいちいち引数に指定しなくていい、もっとすっきりバージョン

FooViewModel.cs (INotifyPropertyChanged実装クラス)
// このメソッドを定義
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

// プロパティのsetアクセサから変更通知
RaisePropertyChanged();

③プロパティをViewにバインドする。

MainWindow.xaml
<TextBox Text="{Binding SampleProperty}"/>

命令のバインド

このボタンが押されたらこの処理をしたい!っていう時に、Windowsフォームだと

Windowsフォーム
private void button1_Click(object o, EventArgs e)
{
}

って感じで、クリック時に呼ばれるメソッドに処理を書く。

いっぽうWPFだと、値のバインドと同じように

WPF(xaml)
<Button Command="{Binding XxxCommand}"/>

こんな感じでボタンと処理を結びつけられる。

ただし、またしてもこれを書いただけでは動かない。
このXxxCommand(ナントカコマンド)ってやつは次に出てくるICommand型である必要がある。これ関係のことも色々せんといかんのだよ。初心者のHPはもうゼロなんだけどあとちょっとだけ頑張ろうね。むり

Command

ICommandを実装するクラス。
ユーザー操作をトリガーとする処理を書くクラス。
このCommandクラスのExecuteメソッドは、WindowsフォームアプリでいうbuttonX_Clickメソッドに(だいたい)相当する。
ViewModelがプロパティに保持することでViewからバインドできるようになる。

①BazCommandは、ICommandを実装する。
ICommandのインタフェースメンバは下記の3つ。

ICommandのインタフェースメンバ
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter);
public void Execute(object parameter);
}

②これらはあらかた実装がパターンとして決まっており、最も単純な場合は下記のようにする。

ICommand実装クラス 最も単純な実装パターン
public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return true; // コマンドの実行条件が特にない場合
        }

        public void Execute(object parameter)
        {
            // ここはコマンドの動作を定義するクラスの核。各自実装。
        }

例えば参考記事のように、Button要素のCommand属性に特定のCommandクラス名をバインドしておけば、ボタン押下時にそのクラスのExecuteメソッドが呼ばれる。

③このコマンドはFooViewModel.csがコンストラクタでnewしてプロパティに保持しておくことで、

FooViewModel.cs
// プロパティ
public BarCommand BarCommand { get; private set; }

// コンストラクタでnew
public FooViewModel()
{
    BarCommand = new BarCommand(this);
}

④Viewから参照(バインド先に指定)できるようになる。

MainWindow.xaml
<Button Command="{Binding BarCommand}"/>

:pushpin:ドボン解決メモ

参考記事とは違った名前空間・フォルダ構造にしたせいで、いろいろドボンした。
このフォルダ構造は実務で見たWPF案件を参考にした。
image.png

ドボン1:初期画面を再設定

MainWindow.xamlをViewsフォルダの中に作り直したので、アプリ起動時に最初に表示する画面の再設定が必要になった。

App.xaml
StartupUri="WpfSampleApp.Views.MainWindow.xaml"  ×
StartupUri="Views.MainWindow.xaml"  ×
StartupUri="Views/MainWindow.xaml"  

StartupUriには名前空間ではなくてあくまでフォルダ構造で指定すればよいらしい。
そうか"URI"やもんな。

ドボン2:ViewとViewModelを紐づける

こちらも名前空間・フォルダ構造を変えたせいで参考記事通りにはいかず修正する必要があった。

1. XAML名前空間を宣言

WpfSampleApp.Views.MainWindow.xaml
<Window xmlns:vm="clr-namespace:WpfSampleApp.ViewModels">
</Window>
  • Window要素 > xmlns属性
    (XML NameSpace)
    xmlns:任意の呼び名="clr-namespace:C#の名前空間"
    でC#の名前空間に任意の呼び名を付けて使うことができる。C#のusingディレクティブみたいな感じ。

  • 用語

    • C#の名前空間 → 「CLR名前空間」という。
    • 任意の呼び名 → 「XAML名前空間」という。

2. DataContextにViewModelを指定

WindowオブジェクトのDataContextプロパティにViewModelに指定するクラス名を指定する。
このとき、1. で宣言した名前空間"vm"を参照している。

WpfSampleApp.Views.MainWindow.xaml
<Window xmlns:vm="clr-namespace:WpfSampleApp.ViewModels">
    <Window.DataContext>
            <vm:CounterViewModel/>
    </Window.DataContext>
</Window>

最後に
WPFのMVVMのポイントは

  • 外観と中身をパキッと分離!
  • しつつ外観と中身を結びつけるバインド機能

参考記事のアプリそのままだとランタイムエラーになる場合があるので、ここの調整を次回以降のお勉強内容としたい。

ひとまず次回は、今回あまり焦点が当たらなかったXAMLに焦点を当ててお勉強したい。


▼次回

41
52
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
52

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?