1. MVVMの概念
MVVM は Model-View-ViewModel の略称で、より一般的な MVC に似たデザイン パターンです。このモデルを導入する主な目的は、フロントエンドの UI ビュー (View) とバックエンドのロジック データ (Model) を分離することで、フロントエンドとフロントエンド間の結合を減らし、開発効率を向上させることです。 、プロジェクトの保守性、拡張性。
- モデル: ビジネス コードを追加せずに、実際のものを単純に抽象化し、モデルをカプセル化します。
- ビュー: フロントエンドのビジュアル UI インターフェイス (ウィンドウ/ページなど)
- ViewModel: フロントエンドにバインドされたビュー モデル。MVC のコントローラーと同様に、インターフェイスに関連する対話ロジックとデータを保存します。
2. MVVMプロジェクトのアーキテクチャ
このセクションでは、簡単なケースを使用して MVVM の組織構造を紹介することを目的としています。このケースの背景は、入力した学生情報を表示するために使用される「OK」と編集情報をクリアするためにそれぞれ使用される「OK」と「クリア」という 2 つのボタンを備えたシンプルな学生情報編集インターフェイスを設計することです。ケース全体の一般的なプロジェクト組織構造は次のとおりです。次のように:
§── コマンド : すべての基本コマンドとその他のカプセル化された再利用可能なコマンド クラスを配置します。
§── コンバーター : すべてのコンバーターを配置します
§── モデル : 場所モデル/抽象エンティティクラス
§── ページ : すべてのページを配置します ページ
§── スタイル : すべてのカスタム コントロール スタイルを配置します。
§── ユーティリティ : すべてのツールを配置します
§── ViewModel: ページにバインドされたすべての ViewModel を配置します
└── Windows : すべてのウィンドウクラスを配置します。
1. コマンド/RelayCommand 基本コマンド クラス
ICommand ベース コマンド クラスをカスタマイズし、Action を委任することでコマンドの内部実行ロジック/動作を実装し、ジェネリックスを通じてコマンドを拡張してロジック処理のフロント エンドとバック エンドの分離を実現します。
namespace WPF_MVVM.Commands
{
//2.默认object命令实现:RelayCommand<object>
public class RelayCommand : RelayCommand<object>
{
public RelayCommand(Action<object> action) : base(action)
{
}
}
//1.自定义ICommand基类:整合泛型,用于在命令内部实现命令逻辑action
public class RelayCommand<T> : ICommand
{
#region Private Members
/// <summary>
/// The _action to run
/// </summary>
private Action<T> _action;
#endregion
#region Constructor
/// <summary>
/// Default constructor
/// </summary>
public RelayCommand(Action<T> action)
{
_action = action;
}
#endregion
#region Command Methods
/// <summary>
/// A relay command can always execute
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public bool CanExecute(object parameter)
{
return true;
}
/// <summary>
/// Executes the commands Action
/// </summary>
/// <param name="parameter"></param>
public void Execute(object parameter)
{
_action((T)parameter);
}
#endregion
#region Public Events
/// <summary>
/// The event thats fired when the <see cref="CanExecute(object)"/> value has changed
/// </summary>
public event EventHandler CanExecuteChanged = (sender, e) => { };
#endregion
}
}
2.モデルの表示
(1) ViewModelBaseの実装
ViewModel は View をバインドし、フロントエンド View にロジック/データ バインディング サポートを提供するために使用されます。そのため、ViewModel は INotifyPropertyChanged、INotifyCollectionChanged などの通知インターフェイスを実装する必要があり、UI データの変更を通知する機能を備えています。そのパブリック部分を基本クラス ViewModelBase としてカプセル化します。
namespace WPF_MVVM.ViewModel
{
public class ViewModelBase : INotifyPropertyChanged, INotifyCollectionChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void RaiseCollectionChanged(ICollection collection)
{
if (CollectionChanged != null)
{
CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public void RaiseCollectionAdd(ICollection collection, object item)
{
if (CollectionChanged != null)
{
if (PropertyChanged != null)
{
PropertyChanged(collection, new PropertyChangedEventArgs("Count"));
PropertyChanged(collection, new PropertyChangedEventArgs("Item[]"));
}
CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
//CollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
}
}
}
(2) MainWindowViewModelの実装
各ウィンドウ Window/page Page には ViewModel がバインドされており、バックグラウンドで View のロジック/データ バインディング サポートを提供する必要があります。この例では、ウィンドウ ページ MainWindow が 1 つだけあるため、MainWindowViewModel も 1 つだけ存在します。
namespace WPF_MVVM.ViewModel
{
public class MainWindowViewModel : ViewModelBase
{
//1.ViewModel 声明为单例模式
private static MainWindowViewModel instance;
public static MainWindowViewModel Instance
{
get
{
if (instance == null)
{
instance = new MainWindowViewModel();
}
return instance;
}
private set { instance = value; }
}
//2.为前端View提供绑定的列表枚举数据(属性方式,字段不行必须声明为静态)
public List<GenderEnum> Genders
{
get
{
return new List<GenderEnum>() { GenderEnum.Male, GenderEnum.Female };
}
}
//3.持有Model对象提供数据绑定Model.xxx(需要先new出来,否则为null)
public Student StudentModel { get; set; } = new Student();
//4.自定义Command绑定的处理方式
public ICommand ConfirmCommand
{
get
{
return new RelayCommand((parameter) =>
{
MessageBox.Show("StudentId: " + StudentModel.StudentId + "\n" +
"StudentName: " + StudentModel.StudentName + "\n" +
"StudentAge: " + StudentModel.StudentAge + "\n" +
"StudentGender: " + StudentModel.StudentGender.ToString());
});
}
}
public ICommand ClearCommand
{
get
{
return new RelayCommand((parameter) =>
{
StudentModel.StudentId = -1;
StudentModel.StudentAge = -1;
StudentModel.StudentName = "null";
StudentModel.StudentGender = GenderEnum.Male;
});
}
}
}
}
3. モデル/学生エンティティ クラス
エンティティ クラスは、エンティティのすべてのプロパティをカプセル化します。ここでエンティティ クラス Student も ViewModelBase を実装する理由は、ビューで Student のプロパティをバインドする必要があり、Student データが変更されたときにフロントエンド ビューに通知できる必要があるため、通知インターフェイスを実装する必要があるためです。 :
- 方法 1: Student のプロパティを MainWindowViewModel にカプセル化し、MainWindowViewModel を通じてデータを直接バインドします。ただし、これの欠点は、ViewModel と Model の違いが明確でなく、Model が不要になることです。
- 方法 2: Student のエンティティ プロパティを Model で個別にカプセル化し、Model オブジェクトを MainWindowViewModel に保持するだけで済みます (新しい必要があります。それ以外の場合は null になります)。そのため、Model レイヤーも ViewModelBase を実装する必要があります。フロントエンド UI プロパティの変更を通知します。ここで 2 番目の方法を説明します。
namespace WPF_MVVM.Model
{
public class Student: ViewModelBase
{
private int studentId;
public int StudentId
{
get
{
return this.studentId;
}
set
{
if (this.studentId != value)
{
this.studentId = value;
RaisePropertyChanged("StudentId");
}
}
}
private string studentName;
public string StudentName
{
get
{
return this.studentName;
}
set
{
if (this.studentName != value)
{
this.studentName = value;
RaisePropertyChanged("StudentName");
}
}
}
private int studentAge;
public int StudentAge
{
get
{
return this.studentAge;
}
set
{
if (this.studentAge != value)
{
this.studentAge = value;
RaisePropertyChanged("StudentAge");
}
}
}
private GenderEnum studentGender;
public GenderEnum StudentGender
{
get
{
return this.studentGender;
}
set
{
if (this.studentGender != value)
{
this.studentGender = value;
RaisePropertyChanged("StudentGender");
}
}
}
}
}
4.Utils/GenderEnum 性別列挙クラス
namespace WPF_MVVM.Utils
{
public enum GenderEnum
{
Male,
Female
}
}
5. メインウィンドウインターフェース
View 部分は非常に単純です。UI インターフェイスを設計するだけで済み、次に Binding メソッドを使用してデータ (ViewModel) と動作 (ICommand) をバインドします。フロント エンドとバック エンドは、単純なデータとコマンドを通じて接続を維持するだけです。それは大きくデカップリングします。
<Window
x:Class="WPF_MVVM.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:enum="clr-namespace:WPF_MVVM.Utils"
xmlns:local="clr-namespace:WPF_MVVM"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:vm="clr-namespace:WPF_MVVM.ViewModel"
Title="MainWindow"
Width="300"
Height="150"
DataContext="{x:Static vm:MainWindowViewModel.Instance}"
mc:Ignorable="d">
<Window.Resources>
<ObjectDataProvider
x:Key="Genders_XAML"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="enum:GenderEnum" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<Style x:Key="LocalTextBoxStyle" TargetType="TextBox">
<Setter Property="Width" Value="135" />
<Setter Property="Margin" Value="0,5,0,5"/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)/ErrorContent}" />
<Setter Property="BorderThickness" Value="0" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal">
<TextBlock Margin="5" Text="学号:" />
<TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentId,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel
Grid.Row="1"
Grid.ColumnSpan="2"
Orientation="Horizontal">
<TextBlock Margin="5" Text="姓名:" />
<TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentName,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel
Grid.Row="2"
Grid.ColumnSpan="2"
Orientation="Horizontal">
<TextBlock Margin="5" Text="年龄:" />
<TextBox Style="{StaticResource LocalTextBoxStyle}" Text="{Binding StudentModel.StudentAge,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel
Grid.Row="3"
Grid.ColumnSpan="2"
Orientation="Horizontal">
<TextBlock Margin="5" Text="性别:" />
<ComboBox
Width="135"
Margin="0,5,0,5"
ItemsSource="{Binding Genders}"
SelectedItem="{Binding StudentModel.StudentGender,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<Button
Grid.Column="2"
Margin="5"
Content="确定">
<Button.InputBindings>
<MouseBinding Command="{Binding ConfirmCommand}" MouseAction="LeftClick"/>
</Button.InputBindings>
</Button>
<Button
Grid.Row="1"
Grid.Column="2"
Margin="5"
Content="清除">
<Button.InputBindings>
<MouseBinding Command="{Binding ClearCommand}" MouseAction="LeftClick"/>
</Button.InputBindings>
</Button>
</Grid>
</Window>
3. 事例の概要
上記のケースでは、基本的に MVVM の組織上の考え方が明確になりました。次に、これまで見たことのない構文がたくさんあることがわかりましたので、ここで簡単な説明と要約を行います。
1. 拘束力の制限
- バインディング ソース ソース: ソースはオブジェクト タイプです。つまり、バインディング ソースはデータを提供できる任意のオブジェクト タイプにすることができます。
- バインディング パス パス: パスは、バインディング ソース [プロパティ] のパス、つまりバインディング ソースのデータ値を設定するために使用されます。ここで、Path は PropertyPath タイプであり、通常のフィールドではなく、プロパティをバインドするためにのみ使用できることに注意してください。実際、フィールドは C# コードを通じてバインドできますが、通常のフィールドではデータ変更後の通知関数を実装できません。
2. ObjectDataProvider の概要
ObjectDataProvider は、XAML でオブジェクトをバインディング ソース オブジェクトとして作成および使用するための便利な方法を提供します。簡単に言えば、ObjectDataProvider は、メソッドの戻り結果を XAML のバインドされたデータ ソースとしてラップして作成できます。enum をデータ バインディング ソースとして直接使用する方法はないため、このメソッドは多くの場合、enum 型自体によって提供される enum 値の配列をデータ バインディング ソースとしてラップするために使用されます。その使用法は次のとおりです ( 「WPF ObjectDataProvider 」を参照)。
この属性を使用して ConstructorParameters パラメーターをオブジェクトのコンストラクターに渡し、コンストラクターを通じてオブジェクトをラップします。
MethodNameプロパティを使用して メソッドを呼び出し、 MethodParameters プロパティを使用してメソッドにパラメータを渡します。その後、データ ソースをメソッドの戻り結果にバインドできます。
- ObjectDataProvider 属性:
- ConstructorParameters:获取要传递给该构造函数的参数列表。
- MethodName: 获取或设置要调用的方法的名称。
- MethodParameters: 获取要传递给方法的参数列表。
- ObjectInstance: 获取或设置用作绑定源的对象。
- ObjectType: 获取或设置要创建其实例的对象的类型。
- 注意: ObjectInstance和ObjectType不可同时赋值。ObjectType的类型是Type,ObjectInstance的类型是Object。换句话说objecttype可以使用x:type进行赋值,也只有这种方式。objectinstance则是可以通过 staticresource的方式进行绑定,绑定的也是实例化后的元素。
バインディング列挙処理では、列挙基本クラス Enum に静的メソッド Enum.GetValues(Type) があり、指定された列挙型の定数値配列 Array を取得して返すことができます。したがって、列挙配列を自分で作成する必要はなく、ObjectDataProvider を通じて処理するだけです。上記のケースバインディング列挙型は次のように書くこともできます。
<Window.Resources>
<ObjectDataProvider
x:Key="Genders_XAML"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="enum:GenderEnum" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<StackPanel
Grid.Row="3"
Grid.ColumnSpan="2"
Orientation="Horizontal">
<TextBlock Margin="5" Text="性别:" />
<ComboBox
Width="135"
Margin="0,5,0,5"
ItemsSource="{Binding Source={StaticResource Genders_XAML}}"
SelectedItem="{Binding StudentModel.StudentGender,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>