📘

【C#】WPFでMVVMをフレームワークなしでシンプルに構築する

2022/09/01に公開

はじめに

WPF でアプリケーションを構築するときに、WindowsForm 癖なのか Xaml ファイル内にイベント作成してロジックを書き込んで・・・というようにしてしまいがちです。
そうなると 1 つのファイルでの役割が多くなりすぎてしまい、コード量が多くなりメンテナンス性が悪くなってしまいます。

そこで MVVM モデルを採用することで Model-ViewModel-View を 3 層に分けることができ、
デザイン=View、画面処理=ViewModel、ロジック=Model というように機能ごとに開発することができます。
こうすることで仕様変更等のコード修正箇所が少なく済むというメリットがあります。

さらに MVVM というのは WPF のために導入されたモデルなので非常に相性が良いというもの採用すべき点です。

MVVM について詳細は以下サイトが参考になります
https://resanaplaza.com/世界で一番短いサンプルで覚えるmvvm入門/

今回はシンプルな MVVM でアプリケーションを作成していこうと思います。

環境

  • Windows10
  • .NET6.0
  • Visual Studio Community 2022
  • WPF

完成イメージ

2 つのテキストボックスを用意して、ボタンをクリックすると入力された文字列が結合されて表示されるという簡単なアプリケーションを作成していきます。
ボタンクリック後

View(画面)

まずは画面を作成していきます。
<Window.DataContext>でバインド先をMainWindowViewModelに指定しています。
こうすることでTextBoxValue1等はViewModelのプロパティを参照して値が変更されるようになります。

MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        xmlns:vm="clr-namespace:WpfApp1.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="490">
    <!--VieModelを指定-->
    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>
    <Grid Background="WhiteSmoke">
        <Label
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            VerticalContentAlignment="Center"
            Margin="10,30,0,0"
            Content="文字列1"
            />
        <TextBox
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            VerticalContentAlignment="Center"
            Height="25"
            Width="150"
            Margin="70,30,0,0"
            Text="{Binding Value1, UpdateSourceTrigger=PropertyChanged}" />
        <Label
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            VerticalContentAlignment="Center"
            Margin="225,30,0,0"
            Content="文字列2"
            />
        <TextBox
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            VerticalContentAlignment="Center"
            Height="25"
            Width="150"
            Margin="280,30,0,0"
            Text="{Binding Value2, UpdateSourceTrigger=PropertyChanged}" />
        <Button
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            Height="40"
            Width="170"
            Margin="150,80,0,0"
            Content="文字列結合"
            Command="{Binding ConcatCommand}"/>
        <Border
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            Width="410"
            Height="250"
            Margin="30,140,0,0"
            BorderBrush="Black"
            BorderThickness="1">
            <TextBlock Text="{Binding ResultValue}" />
        </Border>
    </Grid>
</Window>

各コントロールの値をMainViewModelのプロパティへバインドできたところで次にMainViewModelを実装していきます。

ViewModel(バインド)

ViewModel では変更通知イベントを定義する必要があるためINotifyPropertyChangedというインターフェースを継承します。
インターフェースを実装するとPropertyChangedを作られ、これを実行することでViewModel側でプロパティが変更されたよ、というのはView側へ知らせることができます。

ここではPropertyChangedを実行するメソッドを追加実装し、シンプルな扱いにしました。

MainWindowViewModel.cs
using WpfApp1.Model;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace WpfApp1.ViewModel
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        private MainWindowModel m_Model;

        public MainWindowViewModel()
        {
            m_Model = new MainWindowModel();
            m_concatCommand = new ConcatCommand(this);
        }

        /// <summary>
        /// 通知イベント
        /// </summary>
        public event PropertyChangedEventHandler? PropertyChanged;

        /// <summary>
        /// プロパティの変更通知を起動する
        /// </summary>
        /// <param name="propertyName">プロパティ名</param>
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// 文字列1としてバインドされているプロパティ
        /// </summary>
        public string Value1
        {
            get { return m_Model.Value1; }
            set
            {
                if (m_Model.Value1 != value)
                {
                    m_Model.Value1 = value;
                    NotifyPropertyChanged();
                }
            }
        }

        /// <summary>
        /// 文字列2としてバインドされているプロパティ
        /// </summary>
        public string Value2
        {
            get { return m_Model.Value2; }
            set
            {
                if (m_Model.Value2 != value)
                {
                    m_Model.Value2 = value;
                    NotifyPropertyChanged();
                }
            }
        }

        private string m_resultValue = string.Empty;
        /// <summary>
        /// 結合した文字列
        /// </summary>
        public string ResultValue
        {
            get { return m_resultValue; }
            set
            {
                if (m_resultValue != value)
                {
                    m_resultValue = value;
                    NotifyPropertyChanged();
                }
            }
        }

        private ConcatCommand m_concatCommand;
        /// <summary>
        /// 文字列を結合する処理
        /// </summary>
        public ICommand ConcatCommand => m_concatCommand;
    }
}

さらにView側でバインドしたプロパティを作成したら次にボタンクリック時の処理をCommandに作成していきます。

Command (ボタンクリック処理)

イベント処理を行うにはICommandというインターフェースを継承します。
CanExecuteにこのイベント処理が実行可能な条件、Executeでは実際の処理を行っていきます。
ちなみにparameterは View 側からCommandParameterとして渡された値が入ります。

ConcatCommand.cs
using System;
using System.Windows.Input;
using WpfApp1.ViewModel;

namespace WpfApp1
{
    public class ConcatCommand : ICommand
    {
        MainWindowViewModel m_vm;
        public ConcatCommand(MainWindowViewModel vm)
        {
            m_vm = vm;
        }

        public event EventHandler? CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        /// <summary>
        /// コマンドが利用可能かどうか
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        public bool CanExecute(object? parameter)
        {
            return
                !string.IsNullOrEmpty(m_vm.Value1) && !string.IsNullOrEmpty(m_vm.Value2);
        }

        /// <summary>
        /// 実行時の処理
        /// </summary>
        /// <param name="parameter"></param>
        /// <exception cref="NotImplementedException"></exception>
        public void Execute(object? parameter)
        {
            m_vm.ResultValue = m_vm.Value1 + m_vm.Value2;
        }
    }
}

今回は文字列結合なのでExecuteValue1Value2を結合するようにしています。
では最後のModelの実装をしていきます。

Model(オブジェクト)

Modelが基本的にはデータの入れものの扱いにしています。
DBJSON から取得した値を入れるようオブジェクト作成する必要があります。

ただ今回は外部からデータを取得するわけではないので、サンプルとして必要なデータであるValue1Value2をのみを定義しています。

MainWindowModel.cs
namespace WpfApp1.Model
{
    public class MainWindowModel
    {
        public string Value1 { get; set; } = string.Empty;
        public string Value2 { get; set; } = string.Empty;
    }
}

このMainMWindowodelMainWindowViewModel側でインスタンス生成して互いのValue1Value2が連携するようにしています。

実装後動作

初期画面

MainwindowModelValue1Value2を空白にしているため 2 つのTextBoxは空白です。
さらに、ConcatCommandTextBoxが空白なら利用不可能にしているのでボタンが押せないようになっています。
初期画面

TextBox 入力後

2 つのTextBox両方に文字を入力すると、ConcatCommandCanExecuteTrueになりボタンが有効になります。
TextBox入力後

ボタンクリック後

ボタンをクリックするとConcatCommandExecuteが走り文字列が結合され、結果値がバインドされているTextBlockに表示されます。
ボタンクリック後

まとめ

WPF で MVVM モデルを実装していきました。大規模なアプリケーションであれば MVVM フレームワークであるPrismLivetが使うこともあります。
しかし今回は MVVM の理解を深めるためにもフレームワークなしで構築することで全体像をざっくり掴みやすいのではないかと思い実装しました。

ただ今のままではClickイベント以外の処理を制御したい場合や別ウインドウ起動したいん場合には MVVM モデルでの対応ができていません。
そちらについては以下記事で説明していますので参考にしてみてください。
https://zenn.dev/takuty/articles/e9d8f065452266

参考資料

https://resanaplaza.com/世界で一番短いサンプルで覚えるmvvm入門/
https://elf-mission.net/programming/wpf/getting-started-2020/step02/

GitHubで編集を提案

Discussion