1. サイトトップ
  2. ブログ
  3. WPF
  4. 【WPF】MVVMパターンを採用してツールを作成してみる

【WPF】MVVMパターンを採用してツールを作成してみる

はじめに

こんにちは、情熱開発部プログラム2課の廣江です。

昨今では様々なUIソフトウェアが開発され、皆さんも常日頃使用されているかと思います。
UIソフトウェアを開発するフレームワークは、WPFやSwingなど様々なものがありますが、
今回はタイトルにもある通り、WPFでMVVMパターンを採用してツールの作成を行ってみたいと思います。

WPFは知っている方も多いかと思いますが、Windows Presentation Foundationの略で、マイクロソフトが開発しているUIフレームワークです。
C#でロジック、xamlでUIの実装を行うことが可能です。

MVVMはUIソフトウェアの開発を行ったことがある方でないとあまりなじみのない用語かもしれません。

それでは初めにMVVMの方から説明していきたいと思います。

MVVMとは

UIアーキテクチャの一種で、プログラムの構造を「Model、View、ViewModel」の3つに分割して実装する手法です。

Modelはデータ管理やビジネスロジックなどの実装を行います。

Viewは画面への出力や入力操作の受付を担当します。

ViewModelはMVVMにおいて一番特徴的な存在といえます。
ViewModelは、Viewで変更された値をModelへ渡すのと、その逆で、Modelで変更された値をViewへ渡す機能を実装します。
つまりView – Model間の仲介役のようなものです。

またWPFでのViewModelは、Viewとの通信を行うために、INotifyPropertyChangedを実装する必要があります。

こちらの図は「Model、ViewModel、View」の関係を簡易的に表したものです。

ViewModelとViewの間でプロパティの更新やメソッド呼び出しは少し特殊で、Data Binding、Commandという仕組みを使用します。

Data BindingとCommand

Data BindingとCommandはMVVMの実現に必要な技術です。

Data Binding

Bindingは結合などの意味がありますが、このData Bindingとは、ViewModelのソースプロパティとViewのターゲットプロパティを結合し、値の同期などを行う処理のことを指します。

public class TestViewModel : ViewModel
{
    public bool IsEnable { get; set; }
}
<CheckBox Content="テスト" IsChecked="{Binding Path=IsEnable}"/>

このようなCheckBoxの場合、TestViewModel.IsEnableプロパティ(ソースプロパティ)とCheckBox.IsCheckedプロパティ(ターゲットプロパティ)のData Bindingを行っております。

UIから値を変更すると、CheckBox.IsCheckedが変更され、変更された値がTestViewModel.IsEnableへ適用されます。

またその逆の操作を行うことも可能で、ViewModel側でIsEnableを変更した場合は、CheckBox.IsCheckedへ変更内容が適用され、それに合わせてUIの表示も変化します。

Command

次にCommandについてですが、これはViewからViewModelのメソッドを呼び出すための処理です。

public class TestViewModel : ViewModel
{
    public ICommand TestCommand { get; }
}
<Button Content="テスト" Command="{Binding Path=TestCommand}"/>

WPFではこのようにICommandのプロパティをViewModel側へ追加し、View側のCommandプロパティへバインドして実装します。

Buttonの場合はクリックした際にCommandが実行されるようになっています。

またここで使用しているICommandはインターフェースです。
このインターフェースの実装は自分で行う必要があるので注意してください。

Livetについて

LivetはWPFでMVVMパターンの実装を手助けしてくれるライブラリです。

ViewModelの実装を楽にしてくれるクラスや、先ほど説明したICommandを実装したクラスなどがあります。

LivetにはMVVM関係の他にも、BehaviorやConvertersなどWPFで使用できる機能が実装されています。

こちらはLivetのGitHubのページです。詳細について気になる方はこちらをご参照ください。
runceel/Livet: WPF MVVM Infrastructure.

WPFでMVVMを採用しツールを作成する

それではWPFでMVVMパターンを採用し、ツールを作成してみたいと思います。

開発環境

  • エディタ : Visual Studio2022
  • プロジェクトテンプレート : WPFアプリケーション
  • フレームワーク : .NET 6.0

Livet導入

今回はNuGetパッケージを用いてLivetを導入します。

LivetのNuGetパッケージは用途に合わせていくつかのパッケージが公開されています。

フル機能を使用したい場合はLivetCaskを使用します。

今回はMVVMの機能のみ使用する予定なので、LivetCask.Mvvmパッケージを追加します。

ツール概要

作成するツールですが、サウンドデータをリスト表示し、選択したサウンドを再生するツールを作成してみたいと思います。

次の画像は今回作成したツールの簡易的なクラス図です。

View – ViewModel – Modelで色分けをしてみました。

こちらで何となくイメージできるかもしれませんが、ModelとViewの間にViewModelを挿み、通信などの仲介処理を行っております。

コード説明

それでは、一部のコードを抜擢し解説していきます。

Model

#nullable enable

using System.Collections.ObjectModel;

namespace TestMvvm;

public class SoundListData
{
    public SoundListData()
    {
        Sounds = new( _sounds );
    }

    internal void AddSound(SoundData data)
    {
        _sounds.Add(data);
    }

    internal void Release()
    {
        _sounds.Clear();
    }

    internal ReadOnlyObservableCollection<SoundData> Sounds { get; private set; }

    private ObservableCollection<SoundData> _sounds = new ObservableCollection<SoundData>();
}

こちらはSoundListのModelを実装したクラスです。

Modelの実装に関してはそこまで特殊なことは行っておりませんが、ViewModelへ変更を通知できるようにするために、INotifyCollectionChangedを実装しているObservableCollectionを使用しています。

また今回は簡易的なツールなので、最小限の実装しか行っておりませんが、
本来はこのModel階層に保存処理などの実装を行います。

ViewModel

#nullable enable

using System.Collections.ObjectModel;
using System.Collections.Specialized;

using Livet;

namespace TestMvvm;

public class SoundListViewModel : ViewModel
{
    public SoundListViewModel(SoundListData data)
    {
        CompositeDisposable.Add(release);

        _data = data;
        SoundItems = new();

        if (_data.Sounds is INotifyCollectionChanged collectionChanged)
            collectionChanged.CollectionChanged += onSoundListChanged;

        SelectedItem = null;
    }

  internal void AddSounds(string path)
    {
        _data.AddSound(new(path));
    }

    private void onSoundListChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        switch( e.Action ) {
		case NotifyCollectionChangedAction.Add:
            if (e.NewItems is not null && e.NewItems.Count > 0)
            {
                foreach (var item in e.NewItems)
                {
                    if (item is SoundData data)
                    {
                        SoundItems.Add(new(data));
                    }
                }
                RaisePropertyChanged(nameof(SoundItems));
            }
            break;
        }
    }

    private void release()
    {
        if (_data.Sounds is INotifyCollectionChanged collectionChanged)
            collectionChanged.CollectionChanged -= onSoundListChanged;

        SoundItems.Clear();
        _data.Release();
    }

    public ObservableCollection<SoundItemViewModel> SoundItems { get; private set; }
    public SoundItemViewModel? SelectedItem
    {
        get => selectedItem;
        set => RaisePropertyChangedIfSet<SoundItemViewModel?>(ref selectedItem, value, nameof(SelectedItem));
    }

    private SoundItemViewModel? selectedItem = null;
    private SoundListData _data;
}

こちらはSoundListのViewModelを実装したクラスです。

ViewModelクラスは、Livet.ViewModelを継承して実装して実装します。
またRaisePropertyChangedを呼び出すことで、プロパティの変更を通知することが可能です。
なので、Viewへプロパティの変更を渡したい時はRaisePropertyChangedを呼び出す必要があります。

またViewModelクラスはあくまで仲介役なので、ビジネスロジックなどの実装はできるだけ避ける必要があります。

View

<UserControl x:Class="TestMvvm.SoundListView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:TestMvvm"
             AllowDrop="True"
             PreviewDragOver="onPreviewDragOver"
             Drop="onDrop"
             Background="DimGray">

    <UserControl.Resources>
        <ResourceDictionary>
            <DataTemplate x:Key="_name">
                <TextBlock Text="{Binding Path=Name}" />
            </DataTemplate>

            <GridView	x:Key="_gridView"	x:Shared="False">
                <GridViewColumn Header="SoundName"
                                Width="150"
                                CellTemplate="{StaticResource _name}"/>
            </GridView>
        </ResourceDictionary>
    </UserControl.Resources>
    
    <Grid>
        <ListView Margin="5"
                  HorizontalAlignment="Stretch"
                  Visibility="Visible"
                  SelectionMode="Single"
                  SelectedItem="{Binding Path=SelectedItem}"
                  ItemsSource="{Binding Path=SoundItems, Mode=OneWay}"
                  View="{StaticResource ResourceKey=_gridView}"/>
    </Grid>
</UserControl>

こちらがSoundListViewのxamlです。

SelectedItem="{Binding Path=SelectedItem}"

この行のような、"{Binding Path=~}"などの部分がData Bindingを行っている箇所です。
ここではViewModelのプロパティを指定でき、View – ViewModel間のプロパティの参照・設定を行っています。

ツール

それでは最後になりましたが、こちらが作成したツールの画像です。
下のリストでサウンドを選択し、上のボタンなどで再生、停止等行えるように実装してみました。

今回は簡易的なツールの実装だったので、デザインはほとんど変更していません。勿論ですがWPFはデザインの自由度も高いので、興味がある方は凝ったデザインにしてみるのもいいかもしれません。

まとめ

WPFでMVVMパターンを採用したツールの作成を行いました。

WPFではMVVMが推奨されており今回紹介したLivetなど、開発を手助けしてくれるライブラリもあるので、比較的簡単に実装できると思います。

また、MVVM以外にもUIアーキテクチャは種類があるので、UIソフトウェアの実装を行う際は、このあたりを一度調べてみると、より効率的な実装ができると思います。

UIソフトウェア開発の際など少しでもお役に立てれば幸いです。
最後までご覧いただきありがとうございました。

参考文献


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。