WPF

【C#/WPF実践入門編(10)】MVVMパターン/アーキテクチャの基礎② ~VisualStudioでフォルダ構成とサービス層で実務向け設計~

前回はMVVMパターンの基本概念を学びました。今回は、より実践的なMVVMアプリの設計について学習します。

適切なフォルダ構成でプロジェクトを整理し、サービス層を追加してデータの永続化機能を実装します。

以下の方に役立つ内容となっています。

  • MVVMパターンの実践的な設計手法を習得したい方
  • WPFアプリのフォルダ構成のベストプラクティスを知りたい方
  • WPFアプリにおける永続化機能の基本を学習したい方

この記事では、前回作成したカウンターアプリをベースに、MVVMの典型的なフォルダ構成へのリファクタリングと、JSONファイルによるデータ永続化機能を追加します。

前回の記事から続けて見ていただくと、理解が深まるかと思います。

【C#/WPF実践入門編(9)】MVVMパターン/アーキテクチャの基礎①~Model・View・ViewModelの3層で役割分担~ WPFアプリ開発において重要なアーキテクチャパターンであるMVVM(Model-View-ViewModel)について学びます。 ...
プロ太

WPFアプリの典型的なフォルダ構成を一緒に学びましょう!

演習のコード一式はGitHubに置いてあります。

動画も作成しています。

講義:MVVMの実践的なフォルダ構成

前回の振り返り

前回は、MVVMパターンの基本として以下の3層を学習しました。

  • Model: データとビジネスロジック
  • ViewModel: ViewとModelを繋ぐ仲介役
  • View: UI表示

シンプルな例として、すべてのクラスをプロジェクトルートに配置していました。

しかし、実際のアプリ開発では、適切なフォルダ構成で整理することが重要です。

プロ美

確かに。このまま機能(クラス)が増えていったら、どんどんわかりにくくなりそうだね…。

MVVMの典型的なフォルダ構成

実践的なMVVMアプリケーションでは、以下のようなフォルダ構成が一般的です。各クラスの名前空間もこのフォルダ構成に合わせます。

WpfCounterApp/
├── Commands/          # コマンド関連クラス
│   └── SimpleCommand.cs
├── Models/            # データモデル
│   └── CounterModel.cs
├── Services/          # ビジネスロジック・外部連携
│   └── JsonCounterStorage.cs
├── ViewModels/        # ViewModel
│   └── CounterViewModel.cs
├── Views/             # View(XAML + コードビハインド)
│   ├── CounterWindow.xaml
│   └── CounterWindow.xaml.cs
├── App.xaml
└── App.xaml.cs

この構成により、以下のメリットが得られます。

  • 可読性の向上: 役割ごとにファイルが整理され、目的のクラスを見つけやすい
  • 保守性の向上: 関連するクラス同士が近い場所に配置され、修正時の影響範囲が分かりやすい

C#における名前空間・フォルダ構造の整理の基本については以下の記事も参考にしてください。

C#入門編(11)名前空間とファイル分割 ~Visual Studioでコードを整理整頓~ プログラムが規模を増すにつれ、その管理が徐々に難しくなってくると思います。 これは、プログラム内のクラスやメソッドが増え、それら...
プロ美

Servicesフォルダがあるけど、これはどういう役割?

プロ太

Servicesフォルダは「サービス層」と呼ばれます。これについてもう少し詳しく説明しますね。

サービス層とは?

今回新たに追加するサービス層(Servicesフォルダ)は、以下の役割を担います。

  • 外部システムとの連携(例:ファイル・DBアクセス、Web API)
  • 複雑なビジネスロジック(例:複数モデル連携、複雑な操作)

例えば、今回の演習で追加するデータ永続化機能では、JSONファイルの読み書きをサービス層で実装します。

プロ美

モデル層にもロジックがあったような…。モデル層とサービス層ってどう使い分けるのかな?

プロ太

モデル層では、「データの構造とそのデータに対する基本的なロジック」を扱います。

複数のモデルを扱うロジックや、複雑なロジックについては、サービス層へ配置します。

これにより、モデルをできるだけシンプルに保てます。モデルがシンプルであるほど、責務が明確になり、再利用やテストがしやすくなるためです。

補足: 規模が大きくなった場合の構成

今回紹介した構成はシンプルで学習向きですが、実際の大規模アプリではさらにフォルダを分けることがあります。例えば以下のようなフォルダを作る場合もあります。

  • Repositories/ : データの永続化(DBやAPIアクセス専用)
  • Resources/ : スタイル、テンプレート、文字列リソース(.xaml, .resx など)
  • Utilities/Helpers/ : 共通的な小さな処理(変換、拡張メソッドなど)
  • Styles/ : コントロールやテーマのスタイル定義

また、「Models/Counter」でカウンター機能用のフォルダを作り細分化する・C#プロジェクト自体を複数に分ける、など様々なアプローチがあります。

プロ太

クリーンアーキテクチャなど様々な設計の方法論はありますが、具体的なフォルダ構成は規定されていません。

実際にはチームでの合意や保守性を考慮して柔軟に決めるのが一般的です。

演習:カウンターアプリのリファクタリング+永続化機能追加

作成するアプリの概要

前回作成したカウンターアプリを以下のように拡張します。

  • リファクタリング
    • MVVMの典型的なフォルダ構成への整理
  • 新機能
    • カウンターの増減時に値をJSONファイルへ自動保存
    • アプリ起動時に前回の値を自動復元

以下の手順で実装します。

  • 手順1:フォルダ構成の整理(リファクタリング)
  • 手順2:サービス層として永続化機能を追加(新機能)

手順1:フォルダ構成の整理(リファクタリング)

フォルダを追加

まず、前回作成したプロジェクトを開き、「Commands、Models、Services、ViewModels、Views」フォルダを追加します。

ソリューションエクスプローラで「WpfCounterApp」プロジェクトを右クリックし、「追加 > 新しいフォルダー」で作成していきましょう。

ファイル移動と名前空間の調整

ソリューションエクスプローラ上で、それぞれのファイルを適切なフォルダへ移動し、以下の構成にします。

移動時、以下のダイアログではそれぞれ「OK/はい」を選択しましょう。

プロ太

ファイルを移動すると、クラスの名前空間はVisual Studioによって自動調整(自動リファクタリング)されます。

CounterWindow.xamlについては移動時に自動リファクタリングされないこともあるため、その場合は適切に名前空間を修正しましょう。

CounterWindow.xamlの先頭を以下のように、名前空間+クラス名の部分を、「WpfCounterApp.Views.CounterWindow」へ修正します。

<Window x:Class="WpfCounterApp.Views.CounterWindow"
...

CounterWindow.xaml.csで名前空間を「WpfCounterApp.Views」へ修正します。

using System.Windows;

namespace WpfCounterApp.Views
{
...

XAMLとコードビハインド(●.xaml、●xaml.cs)の名前空間やクラス名が揃っていないと以下のエラーが発生します。

この場合、名前空間・クラス名を揃えましょう。

また、ファイル移動に伴いそれぞれの名前空間が変わることで、以下のようなエラーが発生する場合があります。

この場合、エラー箇所を確認して適切に「using WpfCounterApp.ViewModels;」など、名前空間の参照を追加しましょう。

起動画面のパスを修正

App.xamlで以下のように起動画面のファイルパス(StartupUri)を修正します。

「”CounterWindow.xaml”→”Views/CounterWindow.xaml”」としましょう。
(CounterWindow.xamlファイルの場所を指定しています。)

<Application x:Class="WpfCounterApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfCounterApp"
             StartupUri="Views/CounterWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

起動画面のファイルパスが正しくないと、以下のように実行時にエラーが発生します。この場合、ファイルパス名を確認するようにしましょう。

プロ太

いったんここまでで、ビルド・実行ができるかを確認してみるとよいでしょう。前回と同じように起動・動作すればOKです。

手順2:サービス層として永続化機能を追加(新機能)

サービス層(Servicesフォルダ)にJsonCounterStorage.csを作成します。

using System.IO;
using System.Text.Json;

namespace WpfCounterApp.Services
{
    public class JsonCounterStorage
    {
        private const string FilePath = "counter.json";

        public int Load()
        {
            if (!File.Exists(FilePath)) return 0;

            var json = File.ReadAllText(FilePath);
            return JsonSerializer.Deserialize<int>(json);
        }

        public void Save(int value)
        {
            var json = JsonSerializer.Serialize(value);
            File.WriteAllText(FilePath, json);
        }
    }
}

Model層で、カウンターの初期値を設定できるように、CounterModel.csを修正し、コンストラクタを追加します。

    public class CounterModel
    {
        ...

        public CounterModel(int initialValue = 0)
        {
            Value = initialValue;
        }
        ...

次に、ViewModel層のCounterViewModel.csで、起動時のカウンター値の読み込み、インクリメント・デクリメント時のカウンター値保存の機能を実装しましょう。

★の箇所が修正・追加部分です。

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using WpfCounterApp.Commands;
using WpfCounterApp.Models;
using WpfCounterApp.Services;

namespace WpfCounterApp.ViewModels
{
    public class CounterViewModel : INotifyPropertyChanged
    {
        private readonly CounterModel _model;
        private readonly JsonCounterStorage _storage; //★追加
        private readonly SimpleCommand _incrementCommand;
        private readonly SimpleCommand _decrementCommand;

        public CounterViewModel()
        {
            //★起動時、カウンター値を読み込む
            _storage = new JsonCounterStorage(); 
            int initialValue = _storage.Load(); 
            _model = new CounterModel(initialValue);

            _incrementCommand = new SimpleCommand(_ => ExecuteIncrement());
            _decrementCommand = new SimpleCommand(_ => ExecuteDecrement(), _ => _model.CanDecrement());
        }

        public int Count => _model.Value;

        public ICommand IncrementCommand => _incrementCommand;
        public ICommand DecrementCommand => _decrementCommand;

        private void ExecuteIncrement()
        {
            _model.Increment();
            OnPropertyChanged(nameof(Count));
            _storage.Save(_model.Value); //★変更後の値を保存
        }

        private void ExecuteDecrement()
        {
            _model.Decrement();
            OnPropertyChanged(nameof(Count));
            _storage.Save(_model.Value); //★変更後の値を保存
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

            if (propertyName == nameof(Count))
            {
                _decrementCommand.RaiseCanExecuteChanged();
            }
        }
    }
}
プロ太

今回の機能追加はView層(画面)へは影響しないので、UIコード(XAML)の修正は不要です。なので、これで完成ですね!

最終的なフォルダ構成は以下になっています。

アプリを実行する

カウンターの値を増やしてからアプリを終了し、起動しなおしてみましょう。終了時のカウンター値が復元されるはずです。

デバッグ実行用のフォルダ(例:bin\Debug\net9.0-windows)を覗くと、「counter.json」という永続化用のJSONファイルができているはずです。

JSONファイルの中身は以下のように数字のみですが、これは正しい形式です。

4
プロ美

カウンター値の永続化ができるようになったね!フォルダ構成もスッキリ整理されたよ!

プロ太

これで、実践的なWPFアプリを開発していく土台ができましたね!

まとめ

本記事では、WPF MVVMアプリの実践的な設計について学びました。

フォルダ設計の方針として以下を学びました。

  • 適切なフォルダ構成:Commands、Models、Services、ViewModels、Viewsによる役割別整理
  • サービス層の活用:外部システム連携や複雑なビジネスロジックをサービス層として適切に分離

これらにより、コードの可読性・保守性が向上します。

今回のカウンターアプリの拡張演習を通じて、実際にフォルダ整理によるリファクタリングや、永続化機能(サービス層)の追加を行いました。

今回は、ViewModelやModelは1つだけでしたが、次回はこれらを複数扱う場合の方法について解説する予定です。

プロ太

引き続き、実践的なWPFアプリ開発について一緒に学んでいきましょう!

ABOUT ME
プロ太
●仕事:現在は個人事業主(メンター・情報発信等)、大手IT企業で技術者・マネージャ(15年以上)、大学の外部講師、学生時代は学習塾で非常勤講師(約4年間) ●博士(工学)の学位取得 ●高校生の頃に独学で始め、プログラミング歴20年以上 ●言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ●分野:Webアプリ、テスト自動化、生成AI、デバッガ、コード解析、ドメイン特化言語

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。