【C#】WPFでMVVMをフレームワークなしでシンプルに構築する
はじめに
WPF でアプリケーションを構築するときに、WindowsForm 癖なのか Xaml ファイル内にイベント作成してロジックを書き込んで・・・というようにしてしまいがちです。
そうなると 1 つのファイルでの役割が多くなりすぎてしまい、コード量が多くなりメンテナンス性が悪くなってしまいます。
そこで MVVM モデルを採用することで Model-ViewModel-View を 3 層に分けることができ、
デザイン=View、画面処理=ViewModel、ロジック=Model というように機能ごとに開発することができます。
こうすることで仕様変更等のコード修正箇所が少なく済むというメリットがあります。
さらに MVVM というのは WPF のために導入されたモデルなので非常に相性が良いというもの採用すべき点です。
MVVM について詳細は以下サイトが参考になります
今回はシンプルな MVVM でアプリケーションを作成していこうと思います。
環境
- Windows10
- .NET6.0
- Visual Studio Community 2022
- WPF
完成イメージ
2 つのテキストボックスを用意して、ボタンをクリックすると入力された文字列が結合されて表示されるという簡単なアプリケーションを作成していきます。
View(画面)
まずは画面を作成していきます。
<Window.DataContext>
でバインド先をMainWindowViewModel
に指定しています。
こうすることでTextBox
のValue1
等はViewModel
のプロパティを参照して値が変更されるようになります。
<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
を実行するメソッドを追加実装し、シンプルな扱いにしました。
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
として渡された値が入ります。
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;
}
}
}
今回は文字列結合なのでExecute
にValue1
とValue2
を結合するようにしています。
では最後のModel
の実装をしていきます。
Model(オブジェクト)
Model
が基本的にはデータの入れものの扱いにしています。
DB
や JSON
から取得した値を入れるようオブジェクト作成する必要があります。
ただ今回は外部からデータを取得するわけではないので、サンプルとして必要なデータであるValue1
とValue2
をのみを定義しています。
namespace WpfApp1.Model
{
public class MainWindowModel
{
public string Value1 { get; set; } = string.Empty;
public string Value2 { get; set; } = string.Empty;
}
}
このMainMWindowodel
をMainWindowViewModel
側でインスタンス生成して互いのValue1
とValue2
が連携するようにしています。
実装後動作
初期画面
MainwindowModel
のValue1
とValue2
を空白にしているため 2 つのTextBox
は空白です。
さらに、ConcatCommand
はTextBox
が空白なら利用不可能にしているのでボタンが押せないようになっています。
TextBox 入力後
2 つのTextBox
両方に文字を入力すると、ConcatCommand
のCanExecute
がTrue
になりボタンが有効になります。
ボタンクリック後
ボタンをクリックするとConcatCommand
のExecute
が走り文字列が結合され、結果値がバインドされているTextBlock
に表示されます。
まとめ
WPF で MVVM モデルを実装していきました。大規模なアプリケーションであれば MVVM フレームワークであるPrism
やLivet
が使うこともあります。
しかし今回は MVVM の理解を深めるためにもフレームワークなしで構築することで全体像をざっくり掴みやすいのではないかと思い実装しました。
ただ今のままではClick
イベント以外の処理を制御したい場合や別ウインドウ起動したいん場合には MVVM モデルでの対応ができていません。
そちらについては以下記事で説明していますので参考にしてみてください。
参考資料
Discussion
貴重な記事ありがとうございます。
久しぶりにWPF業務アプリを開発しようとしているところです。
普通長々見にくいXAMLの記述が改行されめちゃ見やすいですが、どのように整形されるのか教えて頂けないでしょうか。よろしくお願いします
コメント頂きありがとうございます!
私も久しく触っていないのでこの記事の時点でのお話になりますが、
XAML Styler
というフォーマッタを使用いたしました 私もタグに対して指定する属性増えてきたらいい感じに改行したいなという思いになり探してみたところこちらがヒットしました。ご参考になれば幸いです 🙇