dotnet newで自分用WPF MVVMテンプレートを作る

コンピュータ

WPFアプリを何度も作っていると、毎回同じファイルを用意したくなります。
例えば、MainWindowViewModel.csRelayCommand.cs
そしてMainWindow.xamlDataContext設定などです。

このような「いつものひな型」は、dotnet new用のオリジナルテンプレートとして登録しておくと便利です。
一度作っておけば、次回からはコマンド一発で自分用の雛形を生成できます。

今回は、WPF向けの最小MVVMテンプレートを例に、dotnet newで使えるテンプレートの作り方をまとめます。

今回作るテンプレートの構成

今回は欲張らず、最小限の構成にします。

MyTemplate
│
├─ App.xaml
├─ App.xaml.cs
├─ MainWindow.xaml
├─ MainWindow.xaml.cs
├─ MainWindowViewModel.cs
├─ RelayCommand.cs
└─ .template.config
   └─ template.json

テンプレート用フォルダは任意でよい

テンプレート用フォルダの場所は任意です。
例えば次のような場所で構いません。

J:\git\MayworkCs\Templates\WpfMvvmTemplate

このフォルダの中に、テンプレート化したいプロジェクト一式と、.template.config/template.jsonを置きます。
特別な場所に置く必要はありません。

テンプレートの中身を用意する

まずは、通常のWPFプロジェクトとして動く状態のファイルを用意します。
その上で、テンプレート用の仮のプロジェクト名を決めておきます。
今回は MyTemplate という名前に統一します。

ここは重要なポイントです。
dotnet newのテンプレートは、名前の意味を理解して変換するのではなく、基本的には文字列置換です。
そのため、namespaceやXAML内の名前は、テンプレート用の仮名に揃えておく必要があります。

MainWindow.xaml

<Window x:Class="MyTemplate.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MyTemplate"
        Title="{Binding Title}"
        Height="240"
        Width="400">

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid Margin="16">
        <StackPanel>
            <TextBlock Text="{Binding Title}" FontSize="20" Margin="0,0,0,12" />
            <Button Content="Click" Command="{Binding ClickCommand}" Width="120" Height="32" />
        </StackPanel>
    </Grid>
</Window>

MainWindowViewModel.cs

using System.Reflection;
using System.ComponentModel;
using System.Windows.Input;

namespace MyTemplate;

public class MainWindowViewModel : INotifyPropertyChanged
{
    string _title = Assembly.GetEntryAssembly()?.GetName().Name ?? "App";

    public string Title
    {
        get => _title;
        set
        {
            if (_title == value) return;
            _title = value;
            OnPropertyChanged(nameof(Title));
        }
    }

    public ICommand ClickCommand { get; }

    public MainWindowViewModel()
    {
        ClickCommand = new RelayCommand(_ =>
        {
            Title = $"{Assembly.GetEntryAssembly()?.GetName().Name ?? "App"} Clicked";
        });
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

RelayCommand.cs

using System;
using System.Windows.Input;

namespace MyTemplate;

public class RelayCommand : ICommand
{
    readonly Action<object?> _execute;
    readonly Func<object?, bool>? _canExecute;

    public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter)
    {
        return _canExecute?.Invoke(parameter) ?? true;
    }

    public void Execute(object? parameter)
    {
        _execute(parameter);
    }

    public event EventHandler? CanExecuteChanged;

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

XAMLでDataContextを指定する最小構成

今回のテンプレートでは、MainWindow.xamlの中で直接DataContextを指定しています。

<Window.DataContext>
    <local:MainWindowViewModel />
</Window.DataContext>

本格的なMVVMでは、DIコンテナやViewModelLocatorなどを使う構成もあります。
ただし、最小テンプレートとしてはこのくらいのシンプルさの方が扱いやすいでしょう。

ウィンドウタイトルにアプリケーション名を入れる

初期タイトルには、実行中アセンブリ名を使っています。

Assembly.GetEntryAssembly()?.GetName().Name ?? "App"

これにより、テンプレートから生成したプロジェクト名が、そのまま初期タイトルに使われます。
小さなことですが、毎回タイトル文字列を直す手間が減ります。

template.json を作る

テンプレートとして認識させるには、.template.config/template.json が必要です。

{
  "$schema": "http://json.schemastore.org/template",
  "author": "maywork",
  "classifications": [ "WPF", "MVVM" ],
  "identity": "Maywork.Wpf.MvvmTemplate",
  "name": "WPF MVVM Minimal Template",
  "shortName": "wpfmvvm-min",
  "sourceName": "MyTemplate",
  "tags": {
    "language": "C#",
    "type": "project"
  }
}

$schema について

$schema は、エディタの補完やバリデーションのための指定です。
必須ではありませんが、書いておくとVS Codeなどで補完が効きやすくなります。

sourceName が重要

今回の重要ポイントは sourceName です。

"sourceName": "MyTemplate"

これにより、テンプレート内にある MyTemplate という文字列が、プロジェクト生成時の新しい名前に置換されます。

例えば、テンプレート内に次のような記述があるとします。

namespace MyTemplate;
<Window x:Class="MyTemplate.MainWindow" ...>
<RootNamespace>MyTemplate</RootNamespace>

これが、例えば次のコマンドでプロジェクトを作ると、

dotnet new wpfmvvm-min -n ImageTool

次のように置換されます。

namespace ImageTool;
<Window x:Class="ImageTool.MainWindow" ...>
<RootNamespace>ImageTool</RootNamespace>

つまり、namespaceが特別に解釈されているわけではなく、あくまで文字列置換です。
そのため、テンプレート内では MyTemplate という名前に揃えておくのがコツです。

csproj もテンプレートに含める

テンプレートには .csproj も含めます。
例えば、次のようなファイルです。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
    <RootNamespace>MyTemplate</RootNamespace>
    <AssemblyName>MyTemplate</AssemblyName>
  </PropertyGroup>

</Project>

パッケージを使いたい場合は、PackageReferenceを書いておけば、生成されたプロジェクトにもそのまま含まれます。
ただし、パッケージバージョンがテンプレートに埋め込まれる点は好みが分かれるところです。

今回のような最小テンプレートなら、まずはNuGetパッケージなしで始める方が記事としても分かりやすいでしょう。

テンプレートをインストールする

テンプレートフォルダに移動して、次のコマンドを実行します。

dotnet new install .

これでローカルテンプレートとして登録されます。

登録後は一覧で確認できます。

dotnet new list

テンプレートから新しいプロジェクトを作る

登録後は、次のようにして新しいWPFプロジェクトを生成できます。

dotnet new wpfmvvm-min -n SampleApp

これで、MainWindowViewModel.csRelayCommand.csを含んだ最小MVVM構成のWPFプロジェクトが作られます。

テンプレートを更新したい場合

テンプレートの内容を変更した場合は、一度アンインストールしてから再度インストールすると分かりやすいです。

dotnet new uninstall .
dotnet new install .

または、一覧に表示されるテンプレートIDを指定してアンインストールしても構いません。

不要なフォルダ・ファイルを削除

binやobj、vscodeを使っているなら.vscodeなどのフォルダは削除しておくほうが良いでしょう。
逆にGitを使っているなら.gitignoreを含めると良いでしょう

感想

大げさな仕組みを用意しなくても、自分用のWPFテンプレートは作れることが確認できました。

毎回コピーしているファイルが決まっているなら、それはもうテンプレート化の価値があります。
特にWPFでは、最初の数ファイルを作る手間が地味に面倒です。

dotnet new対応にしておけば、単なるコピペよりも少し整った形で雛形を再利用できます。
自分専用の開発スターターキットを作る感覚で試してみると面白いでしょう。

まとめ

  • テンプレート用フォルダは任意の場所でよい
  • .template.config/template.json を置けばテンプレート化できる
  • sourceName で指定した文字列が新しいプロジェクト名に置換される
  • WPFでは x:Class、namespace、RootNamespace などを同じ仮名に揃えるのがコツ
  • 最小構成なら MainWindowViewModel.csRelayCommand.cs だけでも十分実用的

まずは最小構成でテンプレート化して、必要になったら少しずつ自分色を足していくのが良さそうです。

コメント