↓たまに信仰心から善良なる女性に防犯を呼びかけるが多分「大きなお世話よっ!@ `ᴥ´ @」と思われている
※この記事は中級者向けです。
※まともに読んだらたぶん1時間以上かかります。
多分同じ視点の記事が存在しないため作成。
本稿の狙いとしては、
- 素の
MVVM
はけっこう煩雑であるのを意識したい - 手軽にViewを追加することにより開発利便性が向上することを、開発プロセスを通じて理解したい
┗Button厨なのでButtonで切り換えたい - Prismの柔軟性を視覚的に意識したい
- ついでに
Livet
と比較したい - Testしやすいというのを体験したい
- 極力分かりやすく
というのがある
結果的にかなりの長文となってしまうので目次も後で付ける。無理やり全部盛りにしたため有料記事で金をとる
1ヵ月以内に10いいね以上いかなかったら割に合わないので消す。
作成の敬意
以前オファーが来たときPrismが指定されてて、そのときは実装方法や仕様を全く知らなかったんで辞退したんですよ。現場で覚えるとかムリだと思うんで、この機会にじっくり取り組みました
参考記事
Prismを使わない例。ContentControl
とDataTemplate
を使い、ViewModelの変更でビューを切り替える実装を紹介
- 欠点 : View数が増えるとコマンド管理が煩雑になる点や、モジュラーな設計が難しい点
Prismのリージョン実装例。Prismと素のWPF両方のgitサンプルがある
参考リファレンス
WPF超絶入門から学び直し1【MVVM】
本稿における<素のMVVM>
は基本的に上記の記事と同じように作ってある。
→Prismの基礎入門(英語)
→PrismのGitリポジトリ
→Bootstrapper and the Shell
,Regions
,View Discovery
,Navigation
,Event Aggregator
が有用
Plain MVVM, Prism,Livetでの簡単な比較表
生成AIに作らせたものなので参考程度にしてほしい
項目 | Plaine MVVM(DataTemplate) | Prism(リージョン) | Livet(DataTemplate+ビヘイビア) |
---|---|---|---|
ボタンの実装 |
RelayCommand でCurrentViewModel を変更。ボタンごとにICommand を定義。 |
DelegateCommand でRequestNavigate を呼び出し。CommandParameter でビュー指定。 |
ビヘイビア(CallMethodAction )でメソッドを直接呼び出し。コマンド定義不要。 |
ボタン追加の容易さ | 新しいICommand とボタンを追加(例:SwitchToViewCCommand )。コード量が増える。 |
CommandParameter を追加(例:<Button CommandParameter="ViewC"/> )。シンプル。 |
新しいメソッドとビヘイビア付きボタンを追加(例:SwitchToViewC )。XAML中心で直感的。 |
視覚的柔軟性 | 領域管理は弱い。背景色(例:青のViewA)で視覚化可能だが、構造が単純。 | リージョンでUI領域を明示。スクリーンショット(例:青のViewA、緑のViewB)や図解で明確。 | ビヘイビアで動的動作を視覚化。スクリーンショット(例:青→緑)や図解で補強可能。 |
ビュー追加プロセス | 1. ビューとViewModelを作成 2. DataTemplate 追加3. コマンド+ボタン追加 |
1. ビューとViewModelを作成 2. RegisterForNavigation<ViewC>() 3. ボタン追加 |
1. ビューとViewModelを作成 2. DataTemplate 追加3. メソッド+ボタン追加 |
拡張性 |
低: ビュー数が増えるとICommand とDataTemplate の管理が煩雑。モジュール分割やナビゲーション履歴は手動実装。 |
高: モジュール分割、ナビゲーションジャーナル、DIコンテナで大規模アプリに対応。リージョンで複数領域を管理可能。 |
中: ビヘイビアやInteractionMessenger で中規模アプリに対応。モジュール分割は可能だが、Prismほど強力ではない。 |
保守性 | 低~中: コードが分散し、ビュー数増加でメンテナンスが困難。命名規則や構造が属人化しやすい。 | 高: DIコンテナとリージョンで一貫した構造。コードの再利用性が高く、チーム開発に適する。 | 中~高: ビヘイビアでコードを簡潔化。日本語ドキュメントで保守が容易だが、Prismほど体系的ではない。 |
開発利便性 | 高(小規模): 設定不要で手軽。ビュー数が多いとコマンド管理が煩雑。 |
高(大規模): 初期設定が必要だが、ビュー追加は1行(RegisterForNavigation )。 |
高(中規模): ビヘイビアでXAML中心の開発が可能。日本語ドキュメントで学習が容易。 |
学習コスト | 低: WPF標準機能のみ。初心者でも理解しやすいが、MVVMの基礎知識が必要。 | 高: DIコンテナ、リージョン、Prismの概念を学ぶ必要。英語リソースが主。 |
中: ビヘイビアやInteractionMessenger の学習が必要だが、日本語ドキュメントが豊富。 |
Button厨への訴求 | ボタンごとにICommand が必要で、追加の手間が増える。直感的だがスケールしない。 |
ボタンのCommandParameter 変更だけで切り替え可能。シンプルで再利用性が高い。 |
ビヘイビアでコマンドを省略。XAMLでボタン実装が直感的で、Button厨に優しい。 |
依存性 | 依存なし(純粋なWPF)。 | Prismライブラリ(例:Prism.Unity )に依存。 |
LivetライブラリとMicrosoft.Xaml.Behaviors.Wpf に依存(軽量)。 |
本稿はVisual Studio 2022
.net 9.0
で動作確認を行った。
多少古くても問題はないと思われる。
Github
※1ヵ月以内にStarが付かない場合は非公開
Plain MVVM(素のMVVM)実装
めんどうくさい(TxT)
実行結果
外観(MainView)
以下の構成としたのだが、結果的にかなり煩雑にならざる負えなかった。
<Window x:Class="DynamicButtonUIChange.MainView"
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:DynamicButtonUIChange"
xmlns:controls="clr-namespace:DynamicButtonUIChange.ButtonCommand"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<GridSplitter Grid.Column="1" VerticalAlignment="Stretch" />
<StackPanel VerticalAlignment="Center">
<Button x:Name="UIChange1" Content="UI1" Margin="20"
+ Command="{Binding ShowViewCommand2}"/>
<Button x:Name="UIChange2" Content="UI2"
Margin="20"/>
<Button x:Name="UIChange3" Content="UI3"
Margin="20"/>
</StackPanel>
</Grid>
</Window>
WPFにおけるデータバインディングの基本事項として
1.Commandに渡すBinding用のプロパティはSetterを持ったPublic プロパティ
2.INotiFichangedインターフェースの継承(MVVMではViewModel層)
である必要がある。
3つのButtonで動的にViewを切り替える
などという厄介な要件を自分で決めてしまったため、下記のような構成になった。3つのViewを橋渡しするMainViewを配置したため実質4層構造になった。
※スマホ版だと正常に画が出ないことがあります。
以下mermaid
ユーザーの視点から見るとUIは一方向に情報が流れているように見えるが、内部的には 双方向のデータバインディングにより、View と ViewModel 間でプロパティが動的に同期されている。
この同期はINotifyPropertyChanged
インターフェースの仕組みによって実現されており、プロパティの値が変更されるたびにイベントが発火し、UIも自動的に更新される。
Gitも参照。
各Viewは下記のように定義した。
View1
View2
View3
悩んだ末、WebView2を利用したシンプルなブラウザにした。
Model(データを管理する層)
この際、基本的にデータクラスであると考えて構わない
実務レベルではデータの取得や更新なども担う
namespace DynamicButtonUIChange.Model
{
public class DataModel2
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Age { get; set; }
}
}
ViewModel(表示ロジック管理層)
Model → View
へと橋渡しする。
→ INotifyPropertyChangedでViewへ変更通知を行う
シングルトンにしたかったので下記のようになった。
RelayCommand
クラスを介して更新処理を行っている。
→ RelayCommand
はICommand interfaceを継承
中間層であり、MVVM patternの要となる層。
従来(例えばWinForm的実装)だとViewModelとModelはほぼ同一のクラス内で書いたりする。
using DynamicButtonUIChange.Command;
using DynamicButtonUIChange.Model;
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows.Input;
namespace DynamicButtonUIChange.ViewModel
{
public class MainViewModel : INotifyPropertyChanged
{
//Viewのシングルトン用
View1 _View1 { get; set; }
View2 _View2 { get; set; }
View3 _View3 { get; set; }
private UserControl? _currentView;
public event PropertyChangedEventHandler? PropertyChanged;
public ICommand ShowView1Command { get; }
public ICommand ShowView2Command { get; }
public ICommand ShowView3Command { get; }
ViewModel2 _ViewModel2 { get; }
public MainViewModel()
{
//Viewの表示状態を保持したい場合はシングルトンにする
_View1 = new View1();
_View2 = new View2();
_View3 = new View3();
//データ保持用のシングルトン
//通常はViewModelだけシングルトンにすればよい
+ _ViewModel2 = new ViewModel2();
// 初期UIを設定
//表示状態をリセットしたいならShowViewメソッド内でNew
ShowViewCommand1 = new RelayCommand(_ => ShowView1());
ShowViewCommand2 = new RelayCommand(_ => ShowView2());
ShowViewCommand3 = new RelayCommand(_ => ShowView3());
}
private void ShowView3()
{
//インスタンスを新しくするならここでNew
CurrentView = _View3;
CurrentView.DataContext = new ViewModel3();
}
private void ShowView2()
{
CurrentView = _View2;
//シングルトン(状態保持)なのでNewしない
+ CurrentView.DataContext = _ViewModel2;
}
private void ShowView1()
{
CurrentView = _View1;
CurrentView.DataContext = new ViewModel1();
}
public UserControl? CurrentView
{
get => _currentView;
set
{
_currentView = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentView)));
}
}
}
}
RelayCommand Class
ICommand
を継承したクラスである。
ICommand インターフェイス
出来るだけ詳細にコメントを入れてみた。
ちゃんと発火する。
Command patternで実装するよりシンプルで済むのが特徴
このクラス一つでMVVMバインディングでのメソッド登録と通知が出来る。
using System.Windows.Input;
using System.Windows.Input;
namespace DynamicButtonUIChange.Command
{
class RelayCommand : ICommand
{
/// <summary>
/// コマンドが実行された際に呼び出されるアクション。
/// </summary>
private readonly Action<object?> _execute;
/// <summary>
/// コマンドが実行可能かどうかを判定する関数。null の場合は常に実行可能とみなされる。
/// </summary>
private readonly Func<object?, bool>? _canExecute;
/// <summary>
/// コマンドが実行可能かどうかを判定する。
/// View(例えばボタンなど)はこのメソッドを参照して、操作の可否を制御する。
/// </summary>
/// <param name="parameter">コマンドに渡されるパラメータ(バインドされているオブジェクトなど)</param>
/// <returns>コマンドが現在有効であれば true、無効であれば false</returns>
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
/// <summary>
/// コマンドが実行されたときに呼び出される。
/// </summary>
/// <param name="parameter">コマンドに渡されるパラメータ</param>
public void Execute(object? parameter) => _execute(parameter);
//コマンドの実行内容および実行可否判定を受け取る。
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
/// <summary>
/// WPF のコマンドシステムがコマンドの状態(CanExecute)を再評価する際に使用されるイベント。
/// このイベントが発火すると、View 側のコントロール(例:Button)は CanExecute を再評価し、ボタンの有効/無効状態を更新する。
///
/// 通常、CommandManager.RequerySuggested イベントに委譲することで、
/// フォーカス変更や入力などのタイミングで自動的に状態が更新される。
/// </summary>
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
View-ViewModel-Modelの処理の流れ
View2にFocusして説明
mermaidによりかなり分かりやすくなった。行ったり来たり。
※スマホ版だと画が出ないです
MainView → View2 → ViewModel2 → DataModel2 → ViewModel2 → View2
ユーザーの操作がView2からViewModel2に伝わる
ViewModel2はユーザーの入力に基づいて処理を行い、必要に応じてModel2にデータ取得や更新を依頼します。
Model2の処理:
Model2はデータクラスであり、データを取得または更新します。
Model2から取得したデータがViewModel2に戻され、ViewModel2はそれをViewで表示可能な形に加工します。
Viewの更新: ViewModelが更新したデータがViewに反映され、UIが更新されます(通常はデータバインディングにより自動的に)。
Prismで実装した場合
※あまり簡易的な実装にならなかったのはAI頼りの弊害だなと
以下の記事を参照した方がもっと簡易的だと思う
→ WPF で Prism を使い UserControl を動的に生成して任意の座標に配置したい
記事内にはGitへのLinkもある。
参考:Prism入門 その1
参考2:WPF開発者に伝えたい、PrismとViewModelの活用法とDataContextへの理解
参考3:WPF Prism を使ってみた
手順は参考3準拠。
仕様(要件)はこれまでと同じ。
1.Prism Template Pack の導入
Prism Blank App(WPF) を選択
新規プロジェクトで選択する。
→プロジェクト内で追加しない方がいいと思われる。
中間画像は省略(参考3を参照)
ここではDryIoc
を選択。
豆知識
DryIoc(Dry Inversion of Control)
読み方:ドライ・アイオック
IoC(Inversion of Control=制御の反転)のための軽量で高速な依存性注入(DI)コンテナ
「Dry(Don’t Repeat Yourself)」な設計思想に基づいているため、冗長な記述を避けることを重視している
ViewModels
フォルダとViews
が最初から作成されており、手間がかからない。Models
フォルダは自分で追加するしかないだろう。
→ Modelは要件により千差万別だから自分で作ってねという事らしい。
尚、Git内のソリューションには素のMVVMも同梱。
MainViewとView
-
独自の名前を付ける場合
1.MainWindowをMainView
に名前変更する(出来ればファイル名も)
┗Visual Studioの名前変更機能を使う事
2.MainWindowViewModel
をMainViewModel
に名前変更する(出来ればファイル名も)
┗{View名}
ViewModelじゃないとBindingされない
嫌なら無理に変更する必要はないが、独自の名前を付たい場合はこのようにする。
ファイル名の変更は必須ではないようだが、一覧性が悪い。
プロジェクト内の構成
俯瞰しやすいように提示しておく。
素のMVVMと違うのはModule
フォルダがある点。
DynamicUIChange_Prism
Command
┗ ButtonCommand.cs
┗ RelayCommand.cs
+ Image
┗ test.png
┗ test2.png
+ Models
┗ DataModel1.cs
┗ DataModel2.cs
┗ DataModel3.cs
+ Module
┗ ModuleInit.cs
+ ViewModels
┗ MainViewModel.cs
┗ MainViewModel
┗ ViewModel1.cs
┗ ViewModel1
┗ ViewModel2.cs
┗ ViewModel3.cs
┗ ViewModel3
+ Views
┗ MainView.xaml
┗ MainView.xaml.cs
┗ View1.xaml
┗ View1.xaml.cs
┗ View2.xaml
┗ View2.xaml.cs
┗ View3.xaml
┗ View3.xaml.cs
定義
単なる外観なので前の項を参照。
Bindingするプロパティ名も変えていない。
変更箇所のみ記載
Prism特有のプロパティに強調表示を入れることにする
App.XAML
大変面倒なんだけど、ここから手を入れないと動かない
using DynamicChange_Prism.Views;
using DynamicUIChange_Prism.Module;
using DynamicUIChange_Prism.ViewModel;
using DynamicUIChange_Prism.Views;
using Prism.DryIoc;
using Prism.Ioc;
using Prism.Modularity;
using System.Windows;
namespace DynamicUIChange_Prism
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
///
///PrismApplicationを継承:CreateShellの実装が必要
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
// アプリケーションのシェル(メインウィンドウ)としてMainViewを解決して返す
return Container.Resolve<MainView>();
}
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
// モジュールカタログにModuleInitモジュールを追加して初期化
moduleCatalog.AddModule<ModuleInit>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 必要ならViewとViewModelを明示的に登録
// ViewとViewModelの関連付けを登録する
containerRegistry.RegisterForNavigation<View1, ViewModel1>();
containerRegistry.RegisterForNavigation<View2, ViewModel2>();
containerRegistry.RegisterForNavigation<View3, ViewModel3>();
// MainViewModelをコンテナに登録
containerRegistry.Register<MainViewModel>();
// MainViewのビューモデルとしてMainViewModelを解決する設定を登録
Prism.Mvvm.ViewModelLocationProvider.Register<MainView>(() => Container.Resolve<MainViewModel>());
}
}
}
}
}
}
Module(モジュール)クラスの作成
※あまり簡易的でないので省いても構わない
[Module(ModuleName = "MyModule")]
属性を追加することで、Prismが自動で検知するようになり、依存性注入(DI)が行われる仕組み
ブレイクポイントを置くと発火しているのがわかる
using DynamicChange_Prism.Views;
using DynamicUIChange_Prism.Views;
using Prism.Ioc; // Prismの依存性注入コンテナ関連のインターフェースを使用
using Prism.Modularity; // Prismのモジュール管理機能を使用するための名前空間
using Prism.Regions; // Prismの地域(Region)管理機能を使用するための名前空間
namespace DynamicUIChange_Prism.Module
{
[Module(ModuleName = "MyModule")] // モジュールの名前を"MyModule"として定義
public class ModuleInit : IModule // PrismのIModuleインターフェースを実装したモジュール初期化クラス
{
private readonly IRegionManager _regionManager; // 地域マネージャーをインジェクションで取得
public ModuleInit(IRegionManager regionManager) // コンストラクタでIRegionManagerを依存性注入
{
_regionManager = regionManager; // 地域マネージャーをフィールドに格納
}
public void OnInitialized(IContainerProvider containerProvider) // モジュールが初期化された後に呼び出されるメソッド
{
// MainRegionにビューを登録するコード(現在コメントアウト)
// _regionManager.RegisterViewWithRegion("MainRegion", typeof(View1));
// _regionManager.RegisterViewWithRegion("MainRegion", typeof(View2));
// _regionManager.RegisterViewWithRegion("MainRegion", typeof(View3));
}
public void RegisterTypes(IContainerRegistry containerRegistry) // 依存性注入コンテナにタイプを登録するメソッド
{
// ナビゲーション用にビューを登録
containerRegistry.RegisterForNavigation<View1>(); // View1をナビゲーション対応として登録
containerRegistry.RegisterForNavigation<View2>(); // View2をナビゲーション対応として登録
containerRegistry.RegisterForNavigation<View3>(); // View3をナビゲーション対応として登録
}
}
}
MainView
素のMVVMの実装をコメントアウトしてある。
比べてみてほしい
<Window x:Class="DynamicUIChange_Prism.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:prism="http://prismlibrary.com/"
+ prism:ViewModelLocator.AutoWireViewModel="True"
Title="{Binding Title}" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<GridSplitter Grid.Column="1" VerticalAlignment="Stretch" />
<StackPanel VerticalAlignment="Center">
<Button x:Name="UIChange1" Content="UI1" Margin="20"
Command="{Binding ShowViewCommand1}"/>
<Button x:Name="UIChange2" Content="UI2"
Margin="20"
Command="{Binding ShowViewCommand2}"
/>
<Button x:Name="UIChange3" Content="UI3"
Margin="20"
Command="{Binding ShowViewCommand3}"
/>
</StackPanel>
<ContentControl
+ prism:RegionManager.RegionName="MainRegion"
Grid.Column="2"/>
</Grid>
</Window>
ViewModel
MainViewModel.cs
using DynamicChange_Prism.Views;
using DynamicUIChange_Prism.Views;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
using System.Windows.Controls;
namespace DynamicUIChange_Prism.ViewModel
{
+ public class MainViewModel : BindableBase
{
private string _title = "Prism Application";
public string Title
{
get { return _title; }
+ set { SetProperty(ref _title, value); }
}
+ private readonly IRegionManager _regionManager;
private UserControl _currentView;
public UserControl CurrentView
{
get => _currentView;
set
{
+ SetProperty(ref _currentView, value);
}
}
+ public DelegateCommand ShowViewCommand1 { get; }
+ public DelegateCommand ShowViewCommand2 { get; }
+ public DelegateCommand ShowViewCommand3 { get; }
//Prismでは登録時にNewされるから不要になる
//View1 _View1 { get; set; }
//View2 _View2 { get; set; }
//View3 _View3 { get; set; }
//ViewModel1 _ViewModel1 { get; }
//ViewModel2 _ViewModel2 { get; }
//ViewModel3 _ViewModel3 { get; set; }
public MainViewModel()
{
+ //Newが一切不要になる
// _View1 = new View1();
// _View2 = new View2();
// _ViewModel1 = new ViewModel1();
// _ViewModel2 = new ViewModel2();
// _ViewModel3 = new ViewModel3();
// 初期UIを設定
+ ShowViewCommand1 = new DelegateCommand(() =>Navigate(nameof(View1)));
+ ShowViewCommand2 = new DelegateCommand(() => Navigate(nameof(View2)));
+ ShowViewCommand3 = new DelegateCommand(() => Navigate(nameof(View3)));
}
private void Navigate(string viewName)
{
+ _regionManager.RequestNavigate("MainRegion", viewName);
}
//以下不要に
////private void ShowView3()
////{
//// _View3 = new View3();
//// CurrentView = _View3;
//// CurrentView.DataContext = _ViewModel3;
////}
////private void ShowView2()
////{
//// CurrentView = _View2;
//// CurrentView.DataContext = _ViewModel2;
////}
////private void ShowView1()
////{
//// CurrentView = _View1;
//// CurrentView.DataContext = _ViewModel1;
////}
}
}
Prism の ViewModelLocator
は View 名に「ViewModel」をつけたクラスを自動的に探すというルールで動作する。
ViewModel1
using DynamicUIChange_Prism.Model;
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace DynamicUIChange_Prism.ViewModel
{
+ public class ViewModel1 : BindableBase
{
private ImageSource _backImageSource;
public ImageSource BackImage
{
get => _backImageSource;
+ set => SetProperty(ref _backImageSource, value);
}
private ImageSource _backImageSource2;
public ImageSource BackImage2
{
get => _backImageSource2;
+ set => SetProperty(ref _backImageSource2, value);
}
//DelegateCommandを使用。
//なお、実装済みならICoommandを継承したRelayCommandをそのまま使っても構わない
+ public DelegateCommand Button1Command { get; }
+ public DelegateCommand Button2Command { get; }
DataModel1 _dataModel1;
public ViewModel1()
{
string current = System.IO.Directory.GetCurrentDirectory();
Button1Command = new DelegateCommand(() => LoadImage1(System.IO.Path.Combine(current, "Image", "test.png")));
Button2Command = new DelegateCommand(() => LoadImage2(System.IO.Path.Combine(current, "Image", "test2.png")));
}
private void LoadImage1(string path)
{
try
{
_dataModel1 = new DataModel1()
{
ModelBackImage = new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute)),
};
BackImage = _dataModel1.ModelBackImage;
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"画像の読み込みに失敗しました: {ex.Message}", "エラー", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
}
}
private void LoadImage2(string path)
{
try
{
_dataModel1 = new DataModel1()
{
ModelBackImage2 = new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute)),
};
BackImage2 = _dataModel1.ModelBackImage2;
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"画像の読み込みに失敗しました: {ex.Message}", "エラー", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
}
}
}
}
ViewModel2
素のMVVMのままでは微妙だったので(実はそのままでも使えるのだが)Prism的実装にしてくれとAIに頼んだ(丸投げしたともいう)
Codeに問題はない。
繰り返すがPlane MVVMのままの実装でも問題はない
ICommandからDelegateCommandに変更。
using DynamicUIChange_Prism.Model; // データモデルを使用
using Prism.Commands; // Prismのコマンド機能(DelegateCommandなど)を使用
using Prism.Mvvm; // PrismのMVVM基盤(BindableBaseなど)を使用
using System;
using System.Collections.ObjectModel; // ObservableCollectionを使用
using System.Windows.Input; // ICommandインターフェースを使用
namespace DynamicUIChange_Prism.ViewModel
{
+ internal class ViewModel2 : BindableBase
{
private ObservableCollection<DataModel2> _people; // バックフィールドを初期化なしで宣言
public ObservableCollection<DataModel2> People
{
get => _people; // プロパティゲッター
+ set => SetProperty(ref _people, value); // プロパティセッターで変更通知をトリガー
}
+ public DelegateCommand UIViewCommand2 { get; } // コマンドプロパティ
public ViewModel2(IRelayCommandFactory commandFactory) // コンストラクタでコマンドファクトリを注入
{
_people = new ObservableCollection<DataModel2>(); // ObservableCollectionの初期化
+ UIViewCommand2 = commandFactory.Create(() => Change_UI()); // コマンドをファクトリで生成
InitializeSampleData(); // サンプルデータの初期化を呼び出し
}
private void InitializeSampleData() // サンプルデータの初期化を別メソッドに分離
{
// サンプルデータ初期化
_people.Add(new DataModel2 { Id = 1, Name = "ひつじ太郎", Age = 25 });
_people.Add(new DataModel2 { Id = 2, Name = "ひつじ花子", Age = 30 });
_people.Add(new DataModel2 { Id = 3, Name = "ひつじ次郎", Age = 21 });
_people.Add(new DataModel2 { Id = 4, Name = "ひつじ長老", Age = 70 });
}
private void Change_UI() // UI変更ロジック
{
InitializeSampleData(); // 新しいサンプルデータを追加
}
}
// コマンドファクトリインターフェース(依存性注入用)
public interface IRelayCommandFactory
{
+ DelegateCommand Create(Action execute); // コマンド生成メソッド
}
// コマンドファクトリのデフォルト実装
+ public class RelayCommandFactory : IRelayCommandFactory
{
+ public DelegateCommand Create(Action execute) // DelegateCommandを生成
{
return new DelegateCommand(execute);
}
}
}
Model(DataModel)
素のMVVMと同じ。
一応、Models
フォルダを作成しておく。
問題点
素のMVVMと全く同じ結果を得られるが、どうしてもView3がシングルトンにならない。 シングルトン登録はしていないのだが。
解決方法:
Region Navigation を使う場合は KeepAlive を意識しないと見かけ上シングルトンになる
ので
IRegionMemberLifetime
をView3に継承させて以下のようにすれば解決する
public partial class View3 : UserControl, IRegionMemberLifetime
{
public View3()
{
InitializeComponent();
Debug.WriteLine("View3 インスタンス生成: " + DateTime.Now);
//ここでブレークポイントを置いた。
}
public bool KeepAlive => false;
英語でググるとかPrismの仕様変更を洗うとか、とにかくドキュメントを探したんだけどまさかRegionが原因だとは。
呼び出し履歴(コールスタック)をAIに推論させたら導き出してきた。僕だけではちょっと無理だったんじゃないか(質問はどうせ知らない奴が大半だから最初から期待してない)
それと、PrismでLogライブラリを使うとしたんだがドキュメント見てもエラーになるばかりで全く進んでいない。Nugetパッケージが問題なのかなんなのか。ドキュメントに齟齬があるように感じられる。
呼び出し履歴を資料として残す
//1つ前
internal unsafe object? InvokeWithNoArgs(object? obj, BindingFlags invokeAttr)
{
Debug.Assert(_argCount == 0);
if ((_strategy & InvokerStrategy.StrategyDetermined_RefArgs) == 0)
{
DetermineStrategy_RefArgs(ref _strategy, ref _invokeFunc_RefArgs, _method, backwardsCompat: true);
}
try
{
return _invokeFunc_RefArgs!(obj, refArguments: null);
}
//2つ前
public static bool TryInterpret(IResolverContext r, Expression expr,
object paramExprs, object paramValues, ParentLambdaArgs parentArgs, bool useFec, out object result)
{
result = null;
switch (expr.NodeType)
{
case ExprType.Constant:
{
result = ((ConstantExpression)expr).Value;
return true;
}
case ExprType.New:
{
var newExpr = (NewExpression)expr;
ConstantExpression a;
#if SUPPORTS_FAST_EXPRESSION_COMPILER
var fewArgCount = newExpr.FewArgumentCount;
if (fewArgCount >= 0)
{
if (fewArgCount == 0)
{
result = newExpr.Constructor.Invoke(ArrayTools.Empty<object>());
return true;
}
//3つ前
public static bool TryInterpretAndUnwrapContainerException(
IResolverContext r, Expression expr, bool useFec, out object result)
{
try
{
return Interpreter.TryInterpret(r, expr, FactoryDelegateCompiler.ResolverContextParamExpr, r, null, useFec, out result);
}
//と呼び出し元が続いている
実行結果
省略。
Livetで実装する
参考Link
Livetの導入 (install)
新規プロジェクトから
Livet project template(.NET 6)(C#)
を選択
View
MainView
XAMLは巣のMVVMと同じ。
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
View1
View2
View3
ViewModel
MainViewModel
using DynamicUIChange_Livet.Views;
using Livet.Commands;
using System.Windows.Controls;
//名前空間が衝突する場合
//LivetのViewModelクラスを継承
namespace DynamicUIChange_Livet.ViewModels
{
+ public class MainViewModel : Livet.ViewModel
{
private string _title = "Prism Application";
public string Title
{
get { return _title; }
+ set { RaisePropertyChanged(); }
}
private UserControl _currentView = null!;
public UserControl CurrentView
{
get => _currentView;
set
{
+ _currentView = value; //CurrentView に値をSetしないとContentsControlに表示されない
+ RaisePropertyChanged(nameof(CurrentView));
}
}
//}
//View1 _View1 { get; set; }
//View2 _View2 { get; set; }
//View3 _View3 { get; set; }
public ViewModelCommand ShowViewCommand1
{ get; }
public ViewModelCommand ShowViewCommand2 { get; }
public ViewModelCommand ShowViewCommand3 { get; }
ViewModel1 _ViewModel1 { get; }
ViewModel2 _ViewModel2 { get; }
ViewModel3 _ViewModel3 { get; set; }
private readonly IRelayCommandFactory _relay;
public MainViewModel()
{
///シングルトン
_ViewModel1 = new ViewModel1();
_ViewModel2 = new ViewModel2();
_ViewModel3 = new ViewModel3();
// 初期UIを設定
+ ShowViewCommand1 = new ViewModelCommand(() => ShowView1());
+ ShowViewCommand2 = new ViewModelCommand(() => ShowView2());
+ ShowViewCommand3 = new ViewModelCommand(() => ShowView3());
}
private void ShowView3()
{
CurrentView = new View3() { DataContext = _ViewModel3 };
}
private void ShowView2()
{
CurrentView = new View2() { DataContext = _ViewModel2 };
}
private void ShowView1()
{
CurrentView = new View1() { DataContext = _ViewModel1 };
}
////private void ShowView3()
////{
//// _View3 = new View3();
//// CurrentView = _View3;
//// CurrentView.DataContext = _ViewModel3;
////}
////private void ShowView2()
////{
//// CurrentView = _View2;
//// CurrentView.DataContext = _ViewModel2;
////}
////private void ShowView1()
////{
//// CurrentView = _View1;
//// CurrentView.DataContext = _ViewModel1;
////}
}
}
ViewModel1
using DynamicUIChange_Livet.Models;
using Livet.Commands;
using System;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace DynamicUIChange_Livet.ViewModels
{
+ public class ViewModel1 : Livet.ViewModel
{
private ImageSource _backImageSource;
public ImageSource BackImage
{
get => _backImageSource;
set
{
+ _backImageSource = value;
+ RaisePropertyChanged();
}
}
private ImageSource _backImageSource2;
public ImageSource BackImage2
{
get => _backImageSource2;
set
{
+ _backImageSource2 = value;
+ RaisePropertyChanged();
}
}
public ViewModelCommand Button1Command { get; }
public ViewModelCommand Button2Command { get; }
DataModel1 _dataModel1;
public ViewModel1()
{
string current = System.IO.Directory.GetCurrentDirectory();
+ Button1Command = new ViewModelCommand(() => LoadImage1(System.IO.Path.Combine(current, "Image", "test.png")));
+ Button2Command = new ViewModelCommand(() => LoadImage2(System.IO.Path.Combine(current, "Image", "test2.png")));
}
private void LoadImage1(string path)
{
try
{
_dataModel1 = new DataModel1()
{
ModelBackImage = new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute)),
};
BackImage = _dataModel1.ModelBackImage;
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"画像の読み込みに失敗しました: {ex.Message}", "エラー", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
}
}
private void LoadImage2(string path)
{
try
{
_dataModel1 = new DataModel1()
{
ModelBackImage2 = new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute)),
};
BackImage2 = _dataModel1.ModelBackImage2;
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"画像の読み込みに失敗しました: {ex.Message}", "エラー", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
}
}
}
}
ViewModel2
using DynamicUIChange_Livet.Views;
using Livet.Commands;
using System.Windows.Controls;
namespace DynamicUIChange_Livet.ViewModels
{
+ public class MainViewModel : Livet.ViewModel
{
private string _title = "Prism Application";
public string Title
{
get { return _title; }
set { RaisePropertyChanged(); }
}
private UserControl _currentView = null!;
public UserControl CurrentView
{
get => _currentView;
set
{
+ _currentView = value; //CurrentView に値をSetしないとContentsControlに表示されない
+ RaisePropertyChanged(nameof(CurrentView));
}
}
//}
//View1 _View1 { get; set; }
//View2 _View2 { get; set; }
//View3 _View3 { get; set; }
+ public ViewModelCommand ShowViewCommand1
{ get; }
+ public ViewModelCommand ShowViewCommand2 { get; }
+ public ViewModelCommand ShowViewCommand3 { get; }
ViewModel1 _ViewModel1 { get; }
ViewModel2 _ViewModel2 { get; }
ViewModel3 _ViewModel3 { get; set; }
public MainViewModel()
{
///シングルトン
_ViewModel1 = new ViewModel1();
_ViewModel2 = new ViewModel2();
_ViewModel3 = new ViewModel3();
// 初期UIを設定
+ ShowViewCommand1 = new ViewModelCommand(() => ShowView1());
+ ShowViewCommand2 = new ViewModelCommand(() => ShowView2());
+ ShowViewCommand3 = new ViewModelCommand(() => ShowView3());
}
private void ShowView3()
{
CurrentView = new View3() { DataContext = _ViewModel3 };
}
private void ShowView2()
{
CurrentView = new View2() { DataContext = _ViewModel2 };
}
private void ShowView1()
{
CurrentView = new View1() { DataContext = _ViewModel1 };
}
////private void ShowView3()
////{
//// _View3 = new View3();
//// CurrentView = _View3;
//// CurrentView.DataContext = _ViewModel3;
////}
////private void ShowView2()
////{
//// CurrentView = _View2;
//// CurrentView.DataContext = _ViewModel2;
////}
////private void ShowView1()
////{
//// CurrentView = _View1;
//// CurrentView.DataContext = _ViewModel1;
////}
}
}
ViewModel3
using DynamicUIChange_Livet.Models;
using Livet.Commands;
using System;
namespace DynamicUIChange_Livet.ViewModels
{
internal class ViewModel3 : Livet.ViewModel
{
public ViewModelCommand UiView3Command { get; }
DataModel3 _datamodel;
public ViewModel3()
{
_datamodel = new DataModel3()
{
UriPath = "https://www.microsoft.com/ja-jp/",
};
+ BrowserUri = new Uri(_datamodel.UriPath);
+ UiView3Command = new ViewModelCommand(() => BrowserView(URLstrings));
}
private void BrowserView(string url)
{
if (string.IsNullOrEmpty(url))
return;
BrowserUri = new Uri(url);
}
private string _URLstrings = null!;
public string URLstrings
{
get => _URLstrings;
set
{
if (_URLstrings != value)
{
_URLstrings = value;
+ RaisePropertyChanged(nameof(BrowserUri));
}
}
}
private Uri _BrowserUri = null!;
public Uri BrowserUri
{
get => _BrowserUri;
set
{
if (_BrowserUri != value)
{
_BrowserUri = value;
+ RaisePropertyChanged(nameof(BrowserUri));
}
}
}
}
}
Model(Data Class)
同じなので割愛。
MVVM patternのテスト容易性を検証する
MVVMパターンのテストしやすいとは、目的とする機能までのUI操作を介さずに直接そのメソッドの実行を検証することである。
通常の密結合な実装(Codeビハインド等)ではこれが不可能である。
今回はxunit
を使用する
同様にして
xunit.runner.visualstudio
も検索してインストール
対象はViewModel1とするが、このままではTestは出来ないので 依存性注入(Dependency Injection) を実装する
尚、今回は最後に作成したDynamicUIChange_Livet.ViewModels
を対象とする
ViewModel1のDI
MainView
依存性を上流に伝播させるため、延々と引数を書く。
using DynamicUIChange_Livet.Views;
using Livet.Commands;
using System.Windows.Controls;
using static DynamicUIChange_Livet.ViewModels.ViewModel1;
namespace DynamicUIChange_Livet.ViewModels
{
public class MainViewModel : Livet.ViewModel
{
private string _title = "Prism Application";
public string Title
{
get { return _title; }
set { RaisePropertyChanged(); }
}
private UserControl _currentView = null!;
public UserControl CurrentView
{
get => _currentView;
set
{
_currentView = value; //CurrentView に値をSetしないとContentsControlに表示されない
RaisePropertyChanged(nameof(CurrentView));
}
}
//}
//View1 _View1 { get; set; }
//View2 _View2 { get; set; }
//View3 _View3 { get; set; }
public ViewModelCommand ShowViewCommand1
{ get; }
public ViewModelCommand ShowViewCommand2 { get; }
public ViewModelCommand ShowViewCommand3 { get; }
ViewModel1 _ViewModel1 { get; }
ViewModel2 _ViewModel2 { get; }
ViewModel3 _ViewModel3 { get; set; }
private readonly IImageLoader _imageLoader;
public MainViewModel()
{
///シングルトン
_ViewModel1 = new ViewModel1(_imageLoader);
_ViewModel2 = new ViewModel2();
_ViewModel3 = new ViewModel3();
// 初期UIを設定
ShowViewCommand1 = new ViewModelCommand(() => ShowView1(_imageLoader));
ShowViewCommand2 = new ViewModelCommand(() => ShowView2());
ShowViewCommand3 = new ViewModelCommand(() => ShowView3());
}
public MainViewModel(IImageLoader imageLoader)
{
_imageLoader = imageLoader;
}
private void ShowView3()
{
CurrentView = new View3() { DataContext = _ViewModel3 };
}
private void ShowView2()
{
CurrentView = new View2() { DataContext = _ViewModel2 };
}
public void ShowView1(IImageLoader ImageLoader)
{
CurrentView = new View1(ImageLoader) { DataContext = _ViewModel1 };
}
}
}
ViewModel1
using DynamicUIChange_Livet.Models;
using Livet.Commands;
using System;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace DynamicUIChange_Livet.ViewModels
{
public class ViewModel1 : Livet.ViewModel
{
+ public interface IImageLoader
{
+ public ImageSource LoadImage(string path);
+
+ public ImageSource LoadImage2(string path);
}
+ private readonly IImageLoader _imageLoader;
private ImageSource _backImageSource;
public ImageSource BackImage
{
get => _backImageSource;
set
{
_backImageSource = value;
RaisePropertyChanged();
}
}
private ImageSource _backImageSource2;
public ImageSource BackImage2
{
get => _backImageSource2;
set
{
_backImageSource2 = value;
RaisePropertyChanged();
}
}
public ViewModelCommand Button1Command { get; }
public ViewModelCommand Button2Command { get; }
private readonly IImageLoader _imageLoader;
//インスタンス共有:Testプロジェクト
public static IImageLoader IImage { get; set; }
DataModel1 _dataModel1;
///オーバーロードすることでTest用DIと両立
public MainViewModel()
{
///シングルトン
_ViewModel1 = new ViewModel1(_imageLoader);
_ViewModel2 = new ViewModel2();
_ViewModel3 = new ViewModel3();
// 初期UIを設定
ShowViewCommand1 = new ViewModelCommand(() => ShowView1(_imageLoader));
ShowViewCommand2 = new ViewModelCommand(() => ShowView2());
ShowViewCommand3 = new ViewModelCommand(() => ShowView3());
}
+ public ViewModel1(IImageLoader imageLoader)
{
//TestプロジェクトはコンソールアプリなのでUIの初期化が不要
//Dependency Injection
+ _imageLoader = imageLoader;
+ IImage = imageLoader;
string current = System.IO.Directory.GetCurrentDirectory();
Button1Command = new ViewModelCommand(() => _imageLoader.LoadImage(System.IO.Path.Combine(current, "Image", "test2.png")));
Button2Command = new ViewModelCommand(() => _imageLoader.LoadImage2(System.IO.Path.Combine(current, "Image", "test2.png")));
}
public ImageSource LoadImage1(string path)
{
try
{
_dataModel1 = new DataModel1()
{
ModelBackImage = new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute)),
};
+ return BackImage = _imageLoader.LoadImage(path);
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"画像の読み込みに失敗しました: {ex.Message}", "エラー", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
return null;
}
}
public void LoadImage2(string path)
{
try
{
_dataModel1 = new DataModel1()
{
ModelBackImage2 = new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute)),
};
BackImage2 = _dataModel1.ModelBackImage2;
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"画像の読み込みに失敗しました: {ex.Message}", "エラー", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
}
}
}
}
Testプロジェクトの作成
外部依存
で右クリック → プロジェクト参照の追加
→ 該当プロジェクトを参照
DynamicUIChange_Livet
をプロジェクト参照する
using DynamicUIChange_Livet.ViewModels;
using Moq;
using System.IO;
using System.Windows.Media.Imaging;
namespace DinumicUIChange_Livet.Test
{
public class ViewModel1Tests
{
Mock<ViewModel1.IImageLoader> mockImageLoader = new();
[Fact]
public void LoadImage1_正常系_画像が設定される()
{
var fakeImage = new BitmapImage();
// Moq を使って ImageLoader の LoadImage メソッドをモック
// 引数に関係なく常に fakeImage を返すよう設定
mockImageLoader.Setup(m => m.LoadImage(It.IsAny<string>())).Returns(fakeImage);
// ViewModel1 にモックの ImageLoader を注入してインスタンス化
var vm = new ViewModel1(mockImageLoader.Object);
// Arrange
string current = Directory.GetCurrentDirectory();
string path = Path.Combine(AppContext.BaseDirectory, "Image", "test.png");
+ ViewModel1.IImageLoader imageLoader = ViewModel1.IImage;
+ vm.BackImage = vm.LoadImage1(path, imageLoader);
Console.WriteLine(path);
Console.WriteLine(vm.ToString());
// Assert
Assert.Equal(fakeImage, vm.BackImage);
mockImageLoader.Verify(m => m.LoadImage(path), Times.Once);
}
//[Fact]
//public void LoadImage1()
//{
// // Arrange
// var mockImageLoader = new Mock<IImageLoader>();
// mockImageLoader.Setup(m => m.LoadImage2(It.IsAny<string>())).Throws(new Exception("失敗"));
// var vm = new ViewModel1(mockImageLoader.Object);
// string current = Directory.GetCurrentDirectory();
// // Act
// vm.LoadImage1(Path.Combine(AppContext.BaseDirectory, @"Image\\test.png"));
// // Assert
// Assert.Null(vm.BackImage);
// // mockMessageService.Verify(m => m.ShowError(It.Is<string>(s => s.Contains("失敗"))), Times.Once);
//}
}
}
testの実行方法
VisualStudioのターミナルで
dotnet Test
を実行
Test結果
VisualStudioのターミナルで``dotnet test``を実行
テスト概要: 合計: 1, 失敗数: 0, 成功数: 1, スキップ済み数: 0, 期間: 3.4 秒
+ 4.3 秒後に 1 件のエラーで失敗しました をビルド
1 件のエラーで失敗しました
XunitにNewton.Jsonの依存関係があるらしい。
NewTon.Jsonを使わない場合は無視していい
>PS C:\TestCode\DynamicButtonUIChange> dotnet test
復元が完了しました (0.5 秒)
DinumicUIChange_Livet 1 件の警告付きで成功しました (0.4 秒) → DinumicUIChange_Livet\bin\Debug\net9.0-windows7.0\DinumicUIChange_Livet.dll
C:\TestCode\DynamicButtonUIChange\DinumicUIChange_Livet\ViewModels\ViewModel1.cs(25,42): warning CS0169: フィールド 'ViewModel1._messageService' は使用されていません
Error:
An assembly specified in the application dependencies manifest (testhost.deps.json) was not found:
+ package: 'Newtonsoft.Json', version: '13.0.3'
path: 'lib/net6.0/Newtonsoft.Json.dll'
ソース `C:\TestCode\DynamicButtonUIChange\DinumicUIChange_Livet\bin\Debug\net9.0-windows7.0\DinumicUIChange_Livet.dll` の Testhost プロセスがエラーで終了しました: Error:
An assembly specified in the application dependencies manifest (testhost.deps.json) was not found:
package: 'Newtonsoft.Json', version: '13.0.3'
path: 'lib/net6.0/Newtonsoft.Json.dll'
。詳細については、診断ログを確認してください。
DinumicUIChange_Livet テスト 1 件のエラーで失敗しました (0.4 秒)
C:\TestCode\DynamicButtonUIChange\DinumicUIChange_Livet\bin\Debug\net9.0-windows7.0\DinumicUIChange_Livet.dll : error TESTRUNABORT: テスト実行が中止されました。
TestProject1 成功しました (0.3 秒) → TestProject1\bin\Debug\net9.0-windows\TestProject1.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.0.0+e341b939fe (64-bit .NET 9.0.6)
[xUnit.net 00:00:00.07] Discovering: TestProject1
[xUnit.net 00:00:00.11] Discovered: TestProject1
[xUnit.net 00:00:00.13] Starting: TestProject1
C:\TestCode\DynamicButtonUIChange\TestProject1\bin\Debug\net9.0-windows\Image\test.png
DynamicUIChange_Livet.ViewModels.ViewModel1
[xUnit.net 00:00:00.36] DinumicUIChange_Livet.Test.ViewModel1Tests.LoadImage1_正常系_画像が設定される [FAIL]
[xUnit.net 00:00:00.36] Assert.Equal() Failure: Values differ
[xUnit.net 00:00:00.36] Expected: System.Windows.Media.Imaging.BitmapImage
[xUnit.net 00:00:00.36] Actual: file:///C:/TestCode/DynamicButtonUIChange/TestProject1/bin/Debug/net9.0-windows/Image/test.png
[xUnit.net 00:00:00.36] Stack Trace:
[xUnit.net 00:00:00.36] C:\TestCode\DynamicButtonUIChange\TestProject1\Test\ViewModel1Tests.cs(39,0): at DinumicUIChange_Livet.Test.ViewModel1Tests.LoadImage1_正常系_画像が設定される()
[xUnit.net 00:00:00.36] at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
[xUnit.net 00:00:00.36] at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
[xUnit.net 00:00:00.37] Finished: TestProject1
TestProject1 テスト 1 件のエラーで失敗しました (1.1 秒)
C:\TestCode\DynamicButtonUIChange\TestProject1\Test\ViewModel1Tests.cs(39): error TESTERROR:
DinumicUIChange_Livet.Test.ViewModel1Tests.LoadImage1_正常系_画像が設定される (187ms): エラー メッセージ: Assert.Equal() Failure: Values differ
Expected: System.Windows.Media.Imaging.BitmapImage
Actual: file:///C:/TestCode/DynamicButtonUIChange/TestProject1/bin/Debug/net9.0-windows/Image/test.png
スタック トレース:
at DinumicUIChange_Livet.Test.ViewModel1Tests.LoadImage1_正常系_画像が設定される() in C:\TestCode\DynamicButtonUIChange\TestProject1\Test\ViewModel1
Tests.cs:line 39
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
テスト概要: 合計: 1, 失敗数: 1, 成功数: 0, スキップ済み数: 0, 期間: 1.8 秒
2.8 秒後に 2 件のエラーと 1 件の警告で失敗しました をビルド
Projectの依存関係の確認
dotnet list package --include-transitive
でチェック
PS C:\TestCode\DynamicButtonUIChange\testProject1>
+ dotnet list package --include-transitive
プロジェクト 'TestProject1' に次のパッケージ参照が含まれています
[net9.0-windows7.0]:
最上位レベル パッケージ 要求済み 解決済み
> coverlet.collector 6.0.2 6.0.2
> Microsoft.NET.Test.Sdk 17.12.0 17.12.0
> Moq 4.20.72 4.20.72
> xunit 2.9.3 2.9.3
> xunit.runner.visualstudio 3.0.0 3.0.0
推移的なパッケージ 解決済み
> Castle.Core 5.1.1
> LivetCask 4.0.1
> LivetCask.Behaviors 4.0.1
> LivetCask.Collections 4.0.1
> LivetCask.Converters 4.0.1
> LivetCask.Core 4.0.2
> LivetCask.EventListeners 4.0.1
> LivetCask.Messaging 4.0.1
> LivetCask.Mvvm 4.0.1
> Microsoft.CodeCoverage 17.12.0
> Microsoft.TestPlatform.ObjectModel 17.12.0
> Microsoft.TestPlatform.TestHost 17.12.0
> Microsoft.Web.WebView2 1.0.3405.78
> Microsoft.Xaml.Behaviors.Wpf 1.1.31
+ > Newtonsoft.Json 13.0.1
> System.Diagnostics.EventLog 6.0.0
> System.Reflection.Metadata 1.6.0
> xunit.abstractions 2.0.3
> xunit.analyzers 1.18.0
> xunit.assert 2.9.3
> xunit.core 2.9.3
> xunit.extensibility.core 2.9.3
> xunit.extensibility.execution 2.9.3
```
//直し方が分からんけど問題はなさそう。
あとがき
仕事前でも1時間ぐらいずつ弄って要約旅行前には終わらせました。多分20時間弱。
いちいちワードを言い換えるのはSEO対策だったりする。
手動で実装すると分かりにくい上に、すぐに境界が曖昧になる(他の実装部分を入れてしまう)ので苦労させられた。
この際、全部InterFaceで制約した方が楽なんじゃなかろうか。
ビジネスロジックがどうとか言われても分かりにくいので、なるべく簡潔に説明することを心がけた。MVVMの各層も日本語訳を試みた(特にViewModel、お前だよ)。
この記事が面白かったらチャンネル登録、クソだったらFuck ass、枯れてると思ったらBadボタン・Badコメントをお願いします。いつも励みになっていますおありがとうございました。