MVVMで、まともなプロジェクトを作るなら……

C#/MVVM において、私が見てきた『いわゆる入門記事』ですと、フォルダが Models/ViewModels/Views 『だけ』になっています。

ですが『本格的なプロジェクト』を作成するためには、この分類では到底足りません。

迷ったことはありませんか? 「behaviorクラスはどこに置けばいいの?」「ModelとViewModelにまたがるクラスはどこに置けばいいの?」 その迷いは真っ当です!

その点について、私なりにサヤ(ChatGPT-4o)と協力して、本稿を書いています。

必要なフォルダとは?

まず、そこそこ大きなプロジェクトで必要な、 Models/ViewModels/Views を省いたフォルダ構成案を一気に羅列します。

Behaviors

言わずと知れた、behaviorを置くフォルダです。 これはViewの延長線上でありながら、Viewに大量に置くと混乱しますよね?

小規模なプロジェクトであれば、Viewsに置けばいいでしょう。

Constants

定数宣言を置くフォルダです。

定数宣言の数が少ない場合、Modelsに置いてしまっても、いいかも知れません。

Converters

型変換の処理を置くフォルダです。 BooleanToVisibilityConverterEnumToStringConverter 等を置くといいでしょう。

Viewsと繋がりを持つ BooleanToVisibilityConverter がある場合、このフォルダは作った方がいいでしょう。 ただし、Behaviors と Converters のファイルが少ない場合、Utils フォルダにまとめてしまってもいいかも知れません。

Enums

列挙型を置くフォルダです……と、言わなくても分かりますよね(笑)

型列挙が少ないうちは、その関連のクラスに書いてもいいでしょう。

Helpers (or Utils)

これは、個人的に強くお勧めします

例えば、4K対応アプリを作成する場合には、このように DpiHelper.cs を作成して、 App.xaml.cs から EnablePerMonitorDpiAwareness() を呼び出すという使い方になります。

同様に Win32API を利用する場合、ここに置くといいでしょう。

※このクラスを利用する場合、Unsafeをプロジェクトで許可する必要があります。

using System.Runtime.InteropServices;

namespace FileHashCraft.Helpers;

public static partial class DpiHelper
{
    /// <summary>
    /// DpiAwarenessを設定する
    /// </summary>
    public static void EnablePerMonitorDpiAwareness()
    {
        // アプリケーションをDPI Awareに設定
        if (Environment.OSVersion.Version.Major >= 6 && Environment.OSVersion.Version.Minor >= 3)
        {
            // 新しい SetProcessDpiAwareness
            SetProcessDpiAwareness(ProcessDpiAwareness.ProcessPerMonitorDpiAware);
        }
        else
        {
            // Windows 8.1以前の場合は、SetProcessDpiAwareを使用する(非推奨)
            SetProcessDPIAware();
        }
    }

    /// <summary>
    /// 新しい DPI Aware で使う引数
    /// </summary>
    private enum ProcessDpiAwareness
    {
        ProcessDpiUnaware = 0,
        ProcessSystemDpiAware = 1,
        ProcessPerMonitorDpiAware = 2
    }

    /// <summary>
    /// 古い DPI Aware
    /// </summary>
    /// <returns></returns>
    [LibraryImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial bool SetProcessDPIAware();

    /// <summary>
    /// 新しいDPI Aware
    /// </summary>
    /// <param name="awareness"></param>
    /// <returns></returns>
    [LibraryImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static partial bool SetProcessDpiAwareness(ProcessDpiAwareness awareness);
}

Messages

CommunityToolkit.Mvvmでの利用を想定していますが、メッセージの定義を行います。 WeakReferenceMessenger 用の定義ですね。

数が少ない場合は Service フォルダに置いても構わないでしょう。

public record CurrentDirectoryChanged(string CurrentDirectoryPath);

Services

これは、個人的に強くお勧めします

ViewModels と Models の両方に関わる処理は、ここに置きましょう。

これは WPFを中心としたMVVM論 で述べた通り、Model が ViewModel を知っていることが MVVM 的によろしくないからです。 Model→ViewModel を許容することは、それ自体が循環参照を招く可能性があり、バグの温床となります。

Resources

リソースファイルを置く場所です。

国際化対応の resource や、テーマを利用する場合はここに置きましょう。

Models/Dto

DTO(Data Transfer Object)の置き場です。

数が少ない場合は直接 Models に置いてもいいかも知れませんが、フォルダを作った方がいいでしょう。

Model層に「データ構造」と「振る舞い(ロジック)」を混ぜたくない時に、特に有効です。

これは、絶対の提案ではありません

随所に補足説明を入れたとおり、この構成が『絶対』とか『完璧』とか言うつもりは毛頭ありません。

たった一つのファイルのためにフォルダを作るくらいなら、例えば Utils フォルダにまとめても、あるいはいいかも知れません。

また、ここでは原則としてルートフォルダを前提としていますが、 規模によっては「ルートフォルダが多すぎる」という事態も起こるかもしれません。

Enums を Constants に入れてしまうなどの亜種は、考えれば幾らでも出てきそうです。

短いですが、以上です (え?まだカップ麺ができあがってない?知らんがな)。

MyApp/
├── Behaviors/
├── Constants/
├── Converters/
├── Enums/
├── Helpers/
├── Messages/
├── Models/
│    └── Dto/
├── Resources/
├── Services/
├── ViewModels/
└── Views/