如何在 MVVM 中编写 ViewModelBase
How to write a ViewModelBase in MVVM
我是 WPF 编程环境的新手。我正在尝试使用 MVVM 设计模式编写程序。
我做了一些研究并阅读了一些与之相关的文章,很多时候我遇到了这个叫做
的东西
ViewModelBase
我知道它是什么..但是我可以具体知道我应该从哪里开始才能写出我自己的 ViewModelBase?就像......真正理解正在发生的事情而不会变得太复杂。谢谢:)
你有一些 nuget 包来实现 MVVM
- MVVM 灯
- MVVM 交叉
- 棱镜
对我来说,MVVM light 对初学者来说更容易,因为它提供了一些代码示例。
所以最好是安装这个 nuget 包,查看生成的代码,如果需要,请返回给我们以获得更多解释。
如果你不知道里面发生了什么,那么使用 MVVM 框架毫无意义。
所以让我们一步一步来构建您自己的 ViewModelBase class。
ViewModelBase class 对所有视图模型都是通用的。让我们将所有常见逻辑移至此 class.
您的 ViewModel 应该实现 INotifyPropertyChanged
(您明白为什么吗?)
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
[CallerMemberName]
属性不是必需的,但它允许您编写:
OnPropertyChanged();
而不是 OnPropertyChanged("SomeProperty");
,因此您将避免在代码中使用字符串常量。示例:
public string FirstName
{
set
{
_firstName = value;
OnPropertyChanged(); //instead of OnPropertyChanged("FirstName") or OnPropertyChanged(nameof(FirstName))
}
get{ return _firstName;}
}
请注意,不再推荐 OnPropertyChanged(() => SomeProperty)
,因为我们在 C# 6 中有 nameof
运算符。
常见的做法是像这样实现调用 PropertyChanged 的属性:
public string FirstName
{
get { return _firstName; }
set { SetProperty(ref _firstName, value); }
}
让我们在您的视图模型库中定义 SetProperty:
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(storage, value))
return false;
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
它只是在 属性 的值发生变化并且 returns 为真时触发 PropertyChanged
事件。当值未更改且 returns false 时,它不会触发事件。基本思想是,SetProperty
方法是虚拟的,您可以更具体地扩展它 class,例如触发验证,或通过调用 PropertyChanging
事件。
这很漂亮。这是您的 ViewModelBase 在此阶段应包含的全部内容。其余的取决于您的项目。例如,您的应用程序使用基于页面的导航,并且您已经编写了自己的 NavigationService 来处理来自 ViewModel 的导航。因此,您可以将 NavigationService 属性 添加到您的 ViewModelBase class,这样您就可以根据需要从所有视图模型访问它。
为了获得更多的可重用性并保持 SRP,我 class 调用了 BindableBase,这几乎就是我们在这里所做的 INotifyPropertyChanged 的实现。我在每个 WPF/UWP/Silverligt/WindowsPhone 解决方案中重复使用这个 class 因为它是通用的。
然后在每个项目中我创建自定义 ViewModelBase class 派生自 BindableBase:
public abstract ViewModelBase : BindableBase
{
//project specific logic for all viewmodels.
//E.g in this project I want to use EventAggregator heavily:
public virtual IEventAggregator () => ServiceLocator.GetInstance<IEventAggregator>()
}
如果我有使用基于页面的导航的应用程序,我还会为页面视图模型指定基础 class。
public abstract PageViewModelBase : ViewModelBase
{
//for example all my pages has title:
public string Title {get; private set;}
}
我可以有另一个 class 用于对话:
public abstract DialogViewModelBase : ViewModelBase
{
private bool? _dialogResult;
public event EventHandler Closing;
public string Title {get; private set;}
public ObservableCollection<DialogButton> DialogButtons { get; }
public bool? DialogResult
{
get { return _dialogResult; }
set { SetProperty(ref _dialogResult, value); }
}
public void Close()
{
Closing?.Invoke(this, EventArgs.Empty);
}
}
在大多数 MVVM 框架中,基本 ViewModel 类 实际上包含很少的代码 - 通常只是 INotifyPropertyChanged 的实现和一些辅助函数。
查看 MVVM Light 的 ViewModelBase and ObservableObject 类 的源代码。 ObservableObject 主要是 INotifyPropertyChanged 实现 - 使用 lambda 表达式而不是 "magic strings" 作为 属性 名称。 ViewModelBase 扩展了 ObservableObject,主要是一种实用方法,用于确定您是否 运行 在 Visual Studio 设计器
中
我喜欢 this BaseVewModel 它为您的视图模型提供了一种非常干净的风格。查看各种 'before' 和 'after' 比较。当然,没有什么是强制性的 - 如果您不喜欢 BaseViewModel 提供的功能,请不要使用它。或者修改它,因为你有源代码。特别要注意的是,有三种不同的方法可以通过更改通知来实现属性 - 选择您 understand/feel 熟悉的复杂程度。
下面的class可以在WPF项目中用作ViewModelBase:
public abstract class ViewModelBase : INotifyPropertyChanged
{
/// <summary>
/// Multicast event for property change notifications.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Checks if a property already matches the desired value. Sets the property and
/// notifies listeners only when necessary.
/// </summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to a property with both getter and setter.</param>
/// <param name="value">Desired value for the property.</param>
/// <param name="propertyName">Name of the property used to notify listeners.This
/// value is optional and can be provided automatically when invoked from compilers that
/// support CallerMemberName.</param>
/// <returns>True if the value was changed, false if the existing value matched the
/// desired value.</returns>
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
// Log.DebugFormat("{0}.{1} = {2}", this.GetType().Name, propertyName, storage);
this.OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Notifies listeners that a property value has changed.
/// </summary>
/// <param name="propertyName">Name of the property used to notify listeners. This
/// value is optional and can be provided automatically when invoked from compilers
/// that support <see cref="CallerMemberNameAttribute"/>.</param>
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
ViewModel class 的一个例子是:
public class MyViewModel : ViewModelBase
{
private int myProperty;
public int MyProperty
{
get { return myProperty; }
set { SetProperty(ref myProperty, value); }
}
}
为了书写方便,可以使用下面的snippet:
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets
xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>OnPropertyChanged</Title>
</Header>
<Snippet>
<SnippetTypes>
<SnippetType>SurroundsWith</SnippetType>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
<Declarations>
<Literal>
<ID>TYPE</ID>
<ToolTip>Property type</ToolTip>
<Default>int</Default>
</Literal>
<Literal>
<ID>NAME1</ID>
<ToolTip>Property name</ToolTip>
<Default>MyProperty</Default>
</Literal>
</Declarations>
<Code Language="CSharp">
<![CDATA[private $TYPE$ _$NAME1$;
public $TYPE$ $NAME1$
{
get => _$NAME1$;
set => SetProperty(ref _$NAME1$, value);
}]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
完整代码可以从 here 下载。
为了今天重新审视这个答案,我想在为 Visual Studio 编写 MVVM 代码时提供额外的生产力改进。
Visual Studio 中的 Intellisense 可以自动创建 SetProperty
样板方法。为此,我在 Window 的 XAML 中设置了 ViewModel(见下文)。然后,每当我引用 {Binding Path=NewProperty}
时,右键单击和 Select Quick Actions and Refactoring...
(或通过 Ctrl .
)。如果未创建 SetProperty
方法,它将在您的 ViewModel class 中自动为您创建。此外,它将生成绑定所需的 属性 和字段。
<Window x:Class="My.Project.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:My.Project"
mc:Ignorable="d"
xmlns:viewmodel="clr-namespace:My.Project.ViewModels" d:DataContext="{d:DesignInstance Type=viewmodel:MainWindowViewModel}"
Title="MainWindow" Height="360" Width="1000">
...
</Window>
但是,这种方法有缺点
INotifyPropertyChanged
未实现,OnPropertyChanged
方法未实现(如果需要)
- 这需要在每个 ViewModel 中完成
- 这是特定于 Visual Studio
好处:
- 一旦在项目中定义了
SetProperty
方法,使用Quick Actions and Refactoring...
选项只会为你生成必要的属性和字段。如果您从 ViewModelBase
. 继承,这也适用
这是 Visual Studio 生成的 SetProperty
方法。
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
// Log.DebugFormat("{0}.{1} = {2}", this.GetType().Name, propertyName, storage);
this.OnPropertyChanged(propertyName);
return true;
}
我是 WPF 编程环境的新手。我正在尝试使用 MVVM 设计模式编写程序。
我做了一些研究并阅读了一些与之相关的文章,很多时候我遇到了这个叫做
的东西ViewModelBase
我知道它是什么..但是我可以具体知道我应该从哪里开始才能写出我自己的 ViewModelBase?就像......真正理解正在发生的事情而不会变得太复杂。谢谢:)
你有一些 nuget 包来实现 MVVM
- MVVM 灯
- MVVM 交叉
- 棱镜
对我来说,MVVM light 对初学者来说更容易,因为它提供了一些代码示例。
所以最好是安装这个 nuget 包,查看生成的代码,如果需要,请返回给我们以获得更多解释。
如果你不知道里面发生了什么,那么使用 MVVM 框架毫无意义。
所以让我们一步一步来构建您自己的 ViewModelBase class。
ViewModelBase class 对所有视图模型都是通用的。让我们将所有常见逻辑移至此 class.
您的 ViewModel 应该实现
INotifyPropertyChanged
(您明白为什么吗?)public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
[CallerMemberName]
属性不是必需的,但它允许您编写:OnPropertyChanged();
而不是OnPropertyChanged("SomeProperty");
,因此您将避免在代码中使用字符串常量。示例:public string FirstName { set { _firstName = value; OnPropertyChanged(); //instead of OnPropertyChanged("FirstName") or OnPropertyChanged(nameof(FirstName)) } get{ return _firstName;} }
请注意,不再推荐
OnPropertyChanged(() => SomeProperty)
,因为我们在 C# 6 中有nameof
运算符。常见的做法是像这样实现调用 PropertyChanged 的属性:
public string FirstName { get { return _firstName; } set { SetProperty(ref _firstName, value); } }
让我们在您的视图模型库中定义 SetProperty:
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "") { if (EqualityComparer<T>.Default.Equals(storage, value)) return false; storage = value; this.OnPropertyChanged(propertyName); return true; }
它只是在 属性 的值发生变化并且 returns 为真时触发
PropertyChanged
事件。当值未更改且 returns false 时,它不会触发事件。基本思想是,SetProperty
方法是虚拟的,您可以更具体地扩展它 class,例如触发验证,或通过调用PropertyChanging
事件。
这很漂亮。这是您的 ViewModelBase 在此阶段应包含的全部内容。其余的取决于您的项目。例如,您的应用程序使用基于页面的导航,并且您已经编写了自己的 NavigationService 来处理来自 ViewModel 的导航。因此,您可以将 NavigationService 属性 添加到您的 ViewModelBase class,这样您就可以根据需要从所有视图模型访问它。
为了获得更多的可重用性并保持 SRP,我 class 调用了 BindableBase,这几乎就是我们在这里所做的 INotifyPropertyChanged 的实现。我在每个 WPF/UWP/Silverligt/WindowsPhone 解决方案中重复使用这个 class 因为它是通用的。
然后在每个项目中我创建自定义 ViewModelBase class 派生自 BindableBase:
public abstract ViewModelBase : BindableBase
{
//project specific logic for all viewmodels.
//E.g in this project I want to use EventAggregator heavily:
public virtual IEventAggregator () => ServiceLocator.GetInstance<IEventAggregator>()
}
如果我有使用基于页面的导航的应用程序,我还会为页面视图模型指定基础 class。
public abstract PageViewModelBase : ViewModelBase
{
//for example all my pages has title:
public string Title {get; private set;}
}
我可以有另一个 class 用于对话:
public abstract DialogViewModelBase : ViewModelBase
{
private bool? _dialogResult;
public event EventHandler Closing;
public string Title {get; private set;}
public ObservableCollection<DialogButton> DialogButtons { get; }
public bool? DialogResult
{
get { return _dialogResult; }
set { SetProperty(ref _dialogResult, value); }
}
public void Close()
{
Closing?.Invoke(this, EventArgs.Empty);
}
}
在大多数 MVVM 框架中,基本 ViewModel 类 实际上包含很少的代码 - 通常只是 INotifyPropertyChanged 的实现和一些辅助函数。
查看 MVVM Light 的 ViewModelBase and ObservableObject 类 的源代码。 ObservableObject 主要是 INotifyPropertyChanged 实现 - 使用 lambda 表达式而不是 "magic strings" 作为 属性 名称。 ViewModelBase 扩展了 ObservableObject,主要是一种实用方法,用于确定您是否 运行 在 Visual Studio 设计器
中我喜欢 this BaseVewModel 它为您的视图模型提供了一种非常干净的风格。查看各种 'before' 和 'after' 比较。当然,没有什么是强制性的 - 如果您不喜欢 BaseViewModel 提供的功能,请不要使用它。或者修改它,因为你有源代码。特别要注意的是,有三种不同的方法可以通过更改通知来实现属性 - 选择您 understand/feel 熟悉的复杂程度。
下面的class可以在WPF项目中用作ViewModelBase:
public abstract class ViewModelBase : INotifyPropertyChanged
{
/// <summary>
/// Multicast event for property change notifications.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Checks if a property already matches the desired value. Sets the property and
/// notifies listeners only when necessary.
/// </summary>
/// <typeparam name="T">Type of the property.</typeparam>
/// <param name="storage">Reference to a property with both getter and setter.</param>
/// <param name="value">Desired value for the property.</param>
/// <param name="propertyName">Name of the property used to notify listeners.This
/// value is optional and can be provided automatically when invoked from compilers that
/// support CallerMemberName.</param>
/// <returns>True if the value was changed, false if the existing value matched the
/// desired value.</returns>
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
// Log.DebugFormat("{0}.{1} = {2}", this.GetType().Name, propertyName, storage);
this.OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Notifies listeners that a property value has changed.
/// </summary>
/// <param name="propertyName">Name of the property used to notify listeners. This
/// value is optional and can be provided automatically when invoked from compilers
/// that support <see cref="CallerMemberNameAttribute"/>.</param>
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
ViewModel class 的一个例子是:
public class MyViewModel : ViewModelBase
{
private int myProperty;
public int MyProperty
{
get { return myProperty; }
set { SetProperty(ref myProperty, value); }
}
}
为了书写方便,可以使用下面的snippet:
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets
xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>OnPropertyChanged</Title>
</Header>
<Snippet>
<SnippetTypes>
<SnippetType>SurroundsWith</SnippetType>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
<Declarations>
<Literal>
<ID>TYPE</ID>
<ToolTip>Property type</ToolTip>
<Default>int</Default>
</Literal>
<Literal>
<ID>NAME1</ID>
<ToolTip>Property name</ToolTip>
<Default>MyProperty</Default>
</Literal>
</Declarations>
<Code Language="CSharp">
<![CDATA[private $TYPE$ _$NAME1$;
public $TYPE$ $NAME1$
{
get => _$NAME1$;
set => SetProperty(ref _$NAME1$, value);
}]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
完整代码可以从 here 下载。
为了今天重新审视这个答案,我想在为 Visual Studio 编写 MVVM 代码时提供额外的生产力改进。
Visual Studio 中的 Intellisense 可以自动创建 SetProperty
样板方法。为此,我在 Window 的 XAML 中设置了 ViewModel(见下文)。然后,每当我引用 {Binding Path=NewProperty}
时,右键单击和 Select Quick Actions and Refactoring...
(或通过 Ctrl .
)。如果未创建 SetProperty
方法,它将在您的 ViewModel class 中自动为您创建。此外,它将生成绑定所需的 属性 和字段。
<Window x:Class="My.Project.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:My.Project"
mc:Ignorable="d"
xmlns:viewmodel="clr-namespace:My.Project.ViewModels" d:DataContext="{d:DesignInstance Type=viewmodel:MainWindowViewModel}"
Title="MainWindow" Height="360" Width="1000">
...
</Window>
但是,这种方法有缺点
INotifyPropertyChanged
未实现,OnPropertyChanged
方法未实现(如果需要)- 这需要在每个 ViewModel 中完成
- 这是特定于 Visual Studio
好处:
- 一旦在项目中定义了
SetProperty
方法,使用Quick Actions and Refactoring...
选项只会为你生成必要的属性和字段。如果您从ViewModelBase
. 继承,这也适用
这是 Visual Studio 生成的 SetProperty
方法。
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
// Log.DebugFormat("{0}.{1} = {2}", this.GetType().Name, propertyName, storage);
this.OnPropertyChanged(propertyName);
return true;
}