LoginSignup
12
27

More than 3 years have passed since last update.

WPF アプリを MVVM で作る

Posted at

概要

前回作った簡易メモ帳アプリを MVVM パターンで作り直してみた
前回のリンク https://qiita.com/seka/items/c5fb091ae0928090bfc6

MVVM のざっくりとした説明

WPF には表示部分(ビュー)と実処理部分(モデル)の疎結合を実現するための仕組みとして
データバインディングとコマンドという機能がある(データバインディングとコマンドの説明は長くなるので省略)
WPF ではこれらの機能を使って MVVM という設計方法がよくとられる
MVVM とは下記の3つに分けて実装を行う方法

  • Model → ビジネスロジック、アプリで扱うデータを持つ
  • ViewModel → ModelとViewの間を取り持つ
  • View → 画面表示を行う

MVVM で作った場合のコード

  • 共通処理をまとめたクラス
    • BasePropertyChanged → INotifyPropertyChanged を継承するクラス(プロパティの変更を通知する)
    • DelegateCommand → ボタンを押したときにイベントを処理するためのコマンドクラス
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Windows.Input;

namespace Wpf_memo_binding
{
    public abstract class BasePropertyChanged : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected void SetValue<Type>(Type src, ref Type dst, string name)
        {
            if (!src.Equals(dst))
            {
                dst = src;
                this.RaisePropertyChanged(name);
            }
        }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace Wpf_memo_binding
{
    public class DelegateCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;
        public Action<object> ExecuteHandler;
        public Func<object, bool> CanExecuteHandler;

        public bool CanExecute(object parameter)
        {
            return (this.CanExecuteHandler == null) ? true : this.CanExecuteHandler(parameter);
        }

        public void Execute(object parameter)
        {
            this.ExecuteHandler?.Invoke(parameter);
        }

        public void RaiseCanExecuteChanged()
        {
            this.CanExecuteChanged(this, null);
        }
    }
}

  • ButtonModel → ButtonのModelクラス
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.IO;
using Microsoft.Win32;

namespace Wpf_memo_binding
{
    public class ButtonModel
    {
        private DelegateCommand command;

        /// <summary>
        /// Buttonのコマンド
        /// </summary>
        public ICommand ButtonCommand
        {
            get
            {
                if (this.command == null)
                {
                    this.command = new DelegateCommand()
                    {
                        ExecuteHandler = OnClickButton
                    };
                }
                return this.command;
            }
        }

        private void OnClickButton(object parameter)
        {
            // 保存用ダイヤログを開く
            SaveFileDialog saveFileDialog1 = new SaveFileDialog();
            saveFileDialog1.InitialDirectory = Environment.CurrentDirectory;
            saveFileDialog1.FileName = TextBoxModel.saveFileName;
            if (saveFileDialog1.ShowDialog() == true)
            {
                System.IO.Stream stream;
                stream = saveFileDialog1.OpenFile();
                if (stream != null)
                {
                    // ファイルに書き込む
                    System.IO.StreamWriter sw = new System.IO.StreamWriter(stream);
                    sw.Write(parameter.ToString());
                    sw.Close();
                    stream.Close();
                }
            }
        }
    }
}

  • TextBoxModel → TextBoxのModelクラス
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace Wpf_memo_binding
{
    public class TextBoxModel : BasePropertyChanged
    {
        public static readonly string saveFileName = @"memo.txt";

        public TextBoxModel()
        {
            // ファイルがあれば起動時に開く
            if (!File.Exists(saveFileName))
            {
                return;
            }
            StreamReader sr = new StreamReader(saveFileName, Encoding.GetEncoding("Shift_JIS"));
            Text = sr.ReadToEnd();
            sr.Close();
        }

        private string text = "";
        public string Text
        {
            get { return this.text; }
            set { this.SetValue<string>(value, ref this.text, "Text"); }
        }
    }
}
  • MainWindowViewModel → ViewModelクラス
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Windows.Input;

namespace Wpf_memo_binding
{
    public class MainWindowViewModel : BasePropertyChanged
    {
        private ButtonModel button = new ButtonModel();
        private TextBoxModel textBox = new TextBoxModel();

        public ButtonModel Button
        {
            get { return this.button; }
            set { this.SetValue<ButtonModel>(value, ref this.button, "Button"); }
        }

        public TextBoxModel TextBox
        {
            get { return this.textBox; }
            set { this.SetValue<TextBoxModel>(value, ref this.textBox, "TextBox"); }
        }
    }
}
  • MainWindow.xaml, MainWindow.xaml.cs → Viewクラス
  • MainWindow.xaml.cs は変更しないので省略
<Window x:Class="Wpf_memo_binding.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:Wpf_memo_binding"
        mc:Ignorable="d"
        Title="メモ帳" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Button x:Name="buttonSave" Content="保存" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="75" Command="{Binding Button.ButtonCommand}" CommandParameter="{Binding TextBox.Text}"/>
        <CheckBox x:Name="checkBoxLock" Content="入力ロック" HorizontalAlignment="Left" Margin="90,13,0,0" VerticalAlignment="Top"/>
        <TextBox x:Name="textBoxMemo" HorizontalAlignment="Left" Height="374.04" Margin="10,34.96,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="772" AcceptsReturn="True" IsReadOnly="{Binding IsChecked, ElementName=checkBoxLock}" Text="{Binding TextBox.Text}"/>
    </Grid>
</Window>

MVVM パターンで WPF 作ったときの手法

  • コントロールに必要なModelクラスを作る
  • Model と View をつなぐための ViewModel クラスを作る
  • MainWindow.xaml の プロパティ>共通>DataContext>新規作成 で ViewModel クラスを選択する
  • Button コントロールの プロパティ>共通>Command>データバインディングの作成>データコンテキスト>ButtonCommand を選択する
  • Button コントロールの プロパティ>共通>CommandParameter>データバインディングの作成>データコンテキスト>Text を選択する
  • TextBox コントロールの プロパティ>共通>Text>データバインディングの作成>データコンテキスト>Text を選択する
  • TextBox コントロールの プロパティ>テキスト>IsReadOnly(隠れてる)>データバインディングの作成>ElementName>CheckBox>IsChecked を選択する
  • xamlは自動生成されたもので一切編集していない

感想

作ってみたはいいが、これが正しい MVVM パターンになってるかどうかはわからない
同じアプリ作るのにこちらは手間がかかりすぎる
プロパティの編集方法もくせがあるため学習コストがかかる(プロパティで変更せず、直接記述する場合はxamlを覚える必要がある)
バグなのかわからないがプロパティの値が編集できなくなることがある(再起動すると直る)
大規模なアプリを作る場合は、分割されてることでメリットが出てくるのかも
MVVM パターンで作っては見たが、Form と同じような作り方のほうがやりやすいし、メリットも特に感じられなかった

12
27
3

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
12
27