WPFでシングルウィンドウアプリケーション(一つのウィンドウだけで完結するアプリ)を作っていると、Viewにコントロールを沢山配置することになり、Viewを構成するXAMLファイルと、データソースとして紐づくViewModelのソースファイルが肥大化しがちです。
役割や機能ごとにファイルを分割できれば良いのですが、調べたところViewはユーザーコントロールで分割出来るようなので試してみました。
プロジェクトの作成
ソースコード
ファイル名:MainWindow.xaml
<Window x:Class="SubViewModel01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SubViewModel01"
mc:Ignorable="d"
Title="{Binding Title.Value}"
Height="450"
Width="800"
xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<i:Interaction.Behaviors>
<local:ViewModelCleanupBehavior />
</i:Interaction.Behaviors>
<Grid>
<local:SubWindow DataContext="{Binding SubViewModel}" Margin="10"/>
</Grid>
</Window>
以下の部分でMainWindowViewModel内のSubViewModelプロパティ(SubViewModelのインスタンス)をユーザーコントロールSubWindowのDataContextにデータソースとしてバインドしています。
<local:SubWindow DataContext="{Binding SubViewModel}" Margin="10"/>
ファイル:MainWindowViewModel.cs
using System.Diagnostics;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
using System.Windows;
namespace SubViewModel01;
public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
#region
// INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
// IDisposable
private CompositeDisposable Disposable { get; } = [];
public void Dispose() => Disposable.Dispose();
#endregion
/**************************************************************************
* プロパティ
**************************************************************************/
public ReactiveProperty<string> Title { get; private set; }
public SubViewModel SubViewModel { get; set; }
public MainWindowViewModel()
{
SubViewModel = new SubViewModel()
.AddTo(this.Disposable);
Title = new ReactiveProperty<string>("タイトル")
.AddTo(this.Disposable);
// MainViewModelはSubViewModelを知っている
SubViewModel.PropertyChanged += (sender, e) =>
{
if (nameof(SubViewModel.Name) == e.PropertyName)
{
MessageBox.Show($"{SubViewModel.Name.Value}", "SubViewModelのName変更受信");
}
if (nameof(SubViewModel.Price) == e.PropertyName)
{
MessageBox.Show($"{SubViewModel.Price.Value}", "SubViewModelのPrice変更受信");
}
};
}
}
MainWindowViewModelのプロパティとしてSubViewModelのインスタンスがセットしています。こちらがMainWindow.xamlを経由しユーザーコントロールのSubWindowのデータソースとしてバインドされます。
MainWindowViewModelからSubViewModelが操作可能ですので、SubViewModelのプロパティの変更イベントで実行される処理をMainWindowViewModelで定義しています。
ファイル名:SubViewModel.cs
using System.Diagnostics;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
namespace SubViewModel01;
public class SubViewModel : INotifyPropertyChanged, IDisposable
{
#region
// INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
// IDisposable
private CompositeDisposable Disposable { get; } = [];
public void Dispose() => Disposable.Dispose();
#endregion
/**************************************************************************
* プロパティ
**************************************************************************/
public ReactiveProperty<string> Name { get; set; }
public ReactiveProperty<int> Price { get; set; }
public SubViewModel()
{
Name = new ReactiveProperty<string>("★")
.AddTo(this.Disposable);
Name.Subscribe(e => OnPropertyChanged(nameof(Name))); // 変更を送信
Price = new ReactiveProperty<int>(88)
.AddTo(this.Disposable);
Price.Subscribe(e=> OnPropertyChanged(nameof(Price))); // 変更を送信
}
}
ユーザーコントロールSubWindowのViewModelに成ります。
SubViewModelはMainWindowViewModelを知らない為、他のクラスでも使いまわし(再利用)しやすい形になっています。
MainWindowViewModelを直接操作することは出来ませんが、SubViewModel内のプロパティの変更をMainWindowViewModelに通知したいので、OnPropertyChanged()を使い変更を送信しています。送信した通知を拾うかどうかはMainWindowViewModel側で決めることが出来ます。(イベントを拾わなくともSubViewModelは機能する)
ファイル名:SubWindow.xaml
<UserControl x:Class="SubViewModel01.SubWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SubViewModel01"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="200">
<StackPanel>
<TextBox Text="{Binding Name.Value}" FontWeight="Bold"/>
<TextBox Text="{Binding Price.Value}" FontWeight="Bold"/>
</StackPanel>
</UserControl>
ユーザーコントロールSubWindowのXAML(View)になります。
基本的にXAMLですのでMainWindow.xamlと同じような書式ですが、最初のタグがWindowの代わりにUserControlとなっててクラス名x:Class
がユーザーコントロールの名前に成ります。
d:DesignHeight="100" d:DesignWidth="200">
がどのような影響があるか未確認
StackPanelで縦積みにTextBoxをレイアウトします。TextBoxはNameとPriceのValueとバインドしていますが、こちらはSubWindowViewModelのプロパティを示しています。
ファイル名:SubWindow.xaml.cs
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SubViewModel01;
/// <summary>
/// Interaction logic for SubWindow.xaml
/// </summary>
public partial class SubWindow : System.Windows.Controls.UserControl
{
public SubWindow()
{
InitializeComponent();
}
}
SubWindow.xamlのコードビハインドになります。
継承元のクラスがWindowではなくSystem.Windows.Controls.UserControlになっているくらいで、MVVMですので基本的な処理はViewModelに任せます。
ファイル名:ViewModelCleanupBehavior.cs
using Microsoft.Xaml.Behaviors;
using System.Windows;
namespace SubViewModel01;
public class ViewModelCleanupBehavior : Behavior<Window>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.Closed += this.WindowClosed;
}
private void WindowClosed(object? sender, EventArgs e)
{
(this.AssociatedObject.DataContext as IDisposable)?.Dispose();
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.Closed -= this.WindowClosed;
}
}
今回のサンプルコードの内容とは関連性はないです。
Windowを終了時紐づいているデータソース(ViewModel)のオブジェクトのDispose()を呼び出しています。
実行
dotnet run
起動直後
ユーザーコントロールSubWindowで定義したテキストボックスが表示されていることを確認
値を変更してみる。
変更された旨のメッセージボックスが表示されるが、こちらはMainWindowViewModel内のコードが実行されています。
感想
サンプルとして用意したユーザーコントロールはシンプルなテキストボックスですが、個人的にはTreeViewやListViewなど比較的複雑なコントロールを別ファイルに切り出して使いたいと考えています。
クラス同士の関係が少し複雑ですので、いつかクラス図を書いて説明出来るようになりたい。
コメント