はじめに
こんにちは。山本れきと申します。弊社内ではC#関連のエキスパートとして活動をしています。
近年色々なサービスを作るにあたりJavaScriptを基盤とするフレームワークや言語でフロントエンドを開発することも楽しいですが、そろそろBlazor WebAssembly とC#によって実際のプロダクト開発をしてみたいと考えています。
そこで、C#の開発モデルのうちWPFなどで用いられるMVVM(Model-View-ViewModel)をフロントエンド開発に組み込むことができるか試します。
そして、開発を続けていく様子を連載記事としてお届けしたいと思います。
ご注意:この記事に掲載しているソースコードとその説明は、プロジェクトの現在の状態です。今後の変更や拡張によって、この記事のソースや解説通りのものとはならなくなる可能性があることをご了承ください。 |
プロジェクト
このプロジェクトはここで進めます。
https://github.com/cct-blog/Oniqys.Blazor Blazor MVVM Components
まずは比較的原始的且つ実直な形でMVVMを実装していきます。現在はこの段階です。
次いでMVVM基盤をデザインします。この作業は記述洗練のための ViewとViewModelの基盤実装が主なところとなります。これは次回以降の内容となります。
MVVMができるようになると実装はどうなるでしょうか。最初に、サンプルを見てみましょう。当記事のサンプルは以下のブランチまたはタグから取得してください。。
https://github.com/cct-blog/Oniqys.Blazor/tree/blog/mvvm1
https://github.com/cct-blog/Oniqys.Blazor/releases/tag/blog%2Fmvvm1
サンプル
サンプルを実行すると、ブラウザが起動し以下のような画面を表示します。
2つのチェックボックスは連動していますが、下のIsSelected は「画面更新」ボタンを押下しないと更新しません。
MVVMで作られた範囲とそうでない範囲をあえて混在させました。
以下の2つのファイルを読むと、動作を変えるためにどのように書き分けているかが分かります。
- Index.razor (View に相当します。この中でViewModelと結合できることは必須です。またDataTemplateを記述できることは重要です)
- IndexViewModel.cs (ViewModelに相当します)
Index.razor
razor ページ(View)にはロジックはほとんど書きません。
MVVMの場合、Viewには「何をどのように表示しているか」という情報のみを記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@using Oniqys.Blazor.Controls @using Oniqys.Blazor.Controls.Panel @using Oniqys.Blazor.ViewModel @using Oniqys.Blazor.Sample.ViewModel; @inherits ViewComponentBase<IndexViewModel> @page "/" <p>2つのチェックボックスはIndexViewModelのItemを共有しているので、操作時に自動的に通知を受けて更新する</p> <CheckableItem DataContext="@DataContext.Item"> <DataTemplate> <span>@context</span> </DataTemplate> </CheckableItem> <CheckableItem DataContext="@DataContext.Item"> <DataTemplate> <span>@context</span> </DataTemplate> </CheckableItem> <p>ここから下は通知を受けるViewに収まっていないので同期しない。</p> <p> IsSelected: @DataContext.Item.IsSelected / IsEnabled: @DataContext.Item.IsEnabled </p> <button @onclick="() => StateHasChanged()">画面更新</button> @code { protected override void OnInitialized() { base.OnInitialized(); DataContext = new IndexViewModel(); } } |
https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm1/Oniqys.Blazor.Sample/Pages/Index.razor
- ViewComponentBase<T>を継承します。
- ページ内にViewとして2つのチェックボックス(CheckableItem)を配置します。CheckableItemのViewModelを設定するために DataContext=”@DataContext.Item”としています。
- OnInitialize または OnInitializeAsync のみ記述します。DataContextにIndexViewModelを代入すると操作結果と表示が連動します。(意図的に連動しない範囲を作ることも可能です)
- DataTemplate内にコンテンツをどのように表示するか記述します。(DataTemplateの仕組みは後述します)
現時点では DataContext と何度も書くことになるのが少し厳しいと感じています。かといって、リフレクションを用いた処理はBlazor WebAssemblyでは危険行為です(ILトリミング後に削除される可能性がある)。ここの吸収が肝となりそうです。 また、razorページでは多くのケースで OnInitializedAsync を用いて非同期処理をすることと思います。非同期通信はViewModelよりも下のModel層で行うことを想定しています。 |
IndexViewModel.cs
Index.razor で表示するデータ構造はここにあります。こちらも最小限の記述となります。
1 2 3 4 5 6 7 8 9 10 11 |
using System.Collections.Generic; using System.ComponentModel; using Oniqys.Blazor.ViewModel; namespace Oniqys.Blazor.Sample.ViewModel { public class IndexViewModel : ViewModelBase { public Selectable Item { get; set; } = new Selectable { IsSelected = true, IsEnabled = true, Content = "Test" }; } } |
- ViewModelBase を継承しています。ほとんどの煩わしい処理(更新時の通知処理など)はこの基底クラスに書かれています。
- チェックボックスのためのプロパティは Item です。Selectable<string> とはこの場合、「選択可能な文字列」を意味する小さなViewModelとなっています。
現時点でこのサンプルにはアプリケーションとしての機能はないのでMVVMのModel相当の部分はありません |
基盤の実装
ViewModelとViewの基底クラスが最初の実装となります。
- ViewModelBase(ViewModelの基底クラス)
- ViewComponentBase<T> (Viewの基底クラス)
- 簡易なDataTemplateの実現
ViewModelBaseの実装
ViewModelに必須となる処理をここにまとめています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
namespace Oniqys.Blazor.ViewModel { public abstract class ViewModelBase : INotifyPropertyChanged { protected bool ObjectChangeProcess<T>(ref T field, T value, [CallerMemberName] string propertyName = null) where T : class { if (field == value) return false; field = value; OnPropertyChanged(propertyName); return true; } // 中略 protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public event PropertyChangedEventHandler PropertyChanged; } } |
https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm1/Oniqys.Blazor/ViewModel/ViewModelBase.cs を抜粋
- WPFにおけるMVVMと同様 INotifyPropertyChanged の実装を含みます。
- プロパティのSetter内で実行する典型的な処理をメソッド化しています。
これを用いた実際のViewModelは Selectable.cs を見ると良いでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace Oniqys.Blazor.ViewModel { public partial class Selectable<TContent> : ViewModelBase { private bool _isSelected; public bool IsSelected { get => _isSelected; set => EquatableValueChangeProcess(ref _isSelected, value); } } } |
https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm1/Oniqys.Blazor/ViewModel/Selectable.cs を抜粋
- バッキングフィールドとプロパティを記述します。
- プロパティのSetterに基底クラスのメソッドを書きます。
ソースジェネレーターを用いてさらなる省略が可能です。これは次回以降、実践していく予定です。また、入れ子となっているViewModelをどのように扱うかは現時点では検討中です。 |
ViewComponentBaseの実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using System; using System.ComponentModel; using Microsoft.AspNetCore.Components; namespace Oniqys.Blazor.Controls { public abstract class ViewComponentBase<TViewModel> : ComponentBase { TViewModel _dataContext; [Parameter] public TViewModel DataContext { get => _dataContext; set { if (_dataContext is INotifyPropertyChanged oldValue) { oldValue.PropertyChanged -= OnPropertyChanged; } _dataContext = value; if (_dataContext is INotifyPropertyChanged newValue) { newValue.PropertyChanged += OnPropertyChanged; } OnPropertyChanged(this, null); } } private void OnPropertyChanged(object sender, PropertyChangedEventArgs args) => StateHasChanged(); } } |
- DataContextの更新時に INotifiPropertyChaged.PropertyChanged のイベント着脱を行います。
- INotifiPropertyChaged.PropertyChangedを受けた場合は StateHasChanged() で画面の更新を促します。
DataTemplateの実現
Blazor WebAssemblyは非常に簡単な仕掛けでDataTemplate相当のものを実現できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@namespace Oniqys.Blazor.Controls @using Oniqys.Blazor.ViewModel @inherits ViewComponentBase<Selectable<TItem>> @typeparam TItem <div style="display: block;"> <span style="display: inline;"> <input type="checkbox" @bind="DataContext.IsSelected" disabled="@(!DataContext.IsEnabled)" /> @DataTemplate(DataContext.Content) </span> </div> @code { [Parameter] public RenderFragment<TItem> DataTemplate { get; set; } } |
https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm1/Oniqys.Blazor/Controls/CheckableItem.razor
- RenderFragment の使い方がカギとなります。DataContextはViewComponentBaseから継承しています。
- 親ページ側に、チェックボックスとともに配置するものを書くことができます。そこにViewModelからの情報を含めることが可能です。
まとめ
ここまでで、最小限MVVMとして動作することは確認できました。
もちろん、解決が必要な問題がいくつかあります。例えばWPFでいうところのPanelやItemsControlに相当するものはまだこれから設計することになります。コントロール内にコントロールを記述する場合のViewModelの入れ子をどのように解消するか。
それらについては、次回以降扱いたいと思います。