bye bye wpf ,python

python编译及打包

 
 

0、背景

Python是一种面向对象的解释型计算机程序设计语言,具有丰富和强大的库,使用其开发产品快速高效。

python的解释特性是将py编译为独有的二进制编码pyc文件,然后对pyc中的指令进行解释执行,但是pyc的反编译却非常简单,可直接反编译为源码,当需要将产品发布到外部环境的时候,源码的保护尤为重要。

基于以上原因,本文将介绍如何将python源码编译pyc,编译成动态链接库.so文件,以及自定义python模块如何打包发布,以便用pip安装自己的python模块。

1、python源码编译至pyc文件

使用python的py_compile模块实现python源码编译pyc。

这里举一个简单的例子:在demo文件夹下有一个demo.py,需要将demo.py编译.pyc。

demo.py内容如下:

def print_hello():

print('hello')

在demo文件夹下新建setup.py,内容如下:

import py_compile

py_compile.compile(r'demo.py',r'demo.pyc')

在shell执行:

cd demo

python setup.py

在demo文件夹下,就会生成demo.pyc文件。

2、python源码编译.so文件

Python源码编译至.so文件的思路是先将py转换为c代码,然后编译c为so文件。

  所需编译环境:

python安装:cython

pip install cython

linux 安装:python-devel,gcc

yum install python-devel

yum install gcc

同样举上述例子:在demo文件夹下有一个demo.py,需要将demo.py编译.so。

demo.py内容如下:

def print_hello():

print('hello')

在demo文件夹下新建setup.py,内容如下:

from distutils.core import setup

from Cython.Build import cythonize

 

setup(ext_modules = cythonize(["demo.py"]))

在shell执行:

cd demo

python setup.py build_ext

在demo文件夹下,就会生成demo.c文件,同时在demo文件夹下生成build文件夹,在build文件夹下包含生成的.so文件。

3、自定义python模块打包发布

将自定义python模块打包发布有两种,一种是将python源码打包发布,一种是将python源码转换至动态链接库.so文件打包发布。下面介绍一下这两种打包方式。

A)、使用python源码打包

同样使用上述例子:在demo文件夹下有一个demo.py,需要将demo.py打包。

在demo文件夹下新建setup.py,内容如下:

from distutils.core import setup

setup(name = ‘demo’,

version = '1.0',

py_modules = ['demo'],

)

在shell执行:

cd demo

python setup.py bdist_wheel

在demo文件夹下,生成dist文件夹,dist文件夹中包含了生成的python模块。

B)、使用python源码编译成.so打包

使用上述例子:在demo文件夹下有一个demo.py,需要将demo.py打包。

首先将python源码转换为c代码:

在demo文件夹下新建generateC.py,内容如下:

from distutils.core import setup

from Cython.Build import cythonize

setup(ext_modules = cythonize(["demo.py"]))

然后将c代码编译打包,过程如下:

在demo文件夹下新建generateWHL.py,内容如下:

from setuptools import setup

from setuptools.dist import Distribution

from distutils.core import Extension

setup(name = 'demo',

version = '1.0',

ext_modules = [Extension("demo",['demo.c'])],

)

将上述两个文件执行,如下:在demo文件夹下新建setup.py,内容如下:

import os

cmd1 = "python generateC.py build_ext"

os.system(cmd1)

cmd2 = "python generateWHL.py bdist_wheel"

os.system(cmd2)

在shell执行:

cd demo

python setup.py

在demo文件夹下,生成dist文件夹,dist文件夹中包含了生成的python模块。

C)、安装卸载

可以使用pip直接安装和卸载生成的python模块。

4、其他

本文主要是针对python源码编译打包做了简单介绍,使用了最简单的例子。对于复杂的情况,比如打包时需要额外的数据文件,依赖包等等,需要具体查看setuptools模块的相关内容。

Python文件编译或打包成exe文件,直接在其它Windows电脑上运行_python编译成exe_若如初见kk的博客-CSDN博客

将Python文件编译成exe文件后,可以直接在Windows上运行,不需要再依赖Python环境,可以复制到其他电脑中直接使用,特别方便。

 

1. 安装编译工具

pyinstaller是Python中将py文件编译成为exe文件的免费工具,特别好用,在Windows中,pyinstaller依赖pywin32,所在如果打包有问题请先安装pywin32模块,安装命令如下:

pip install pywin32
pip install pyinstaller

2. 编译命令

2.1 运行时出现dos命令窗口

常用编译命令为:pyinstaller -F 待编译目标文件
例如:

pyinstaller -F test.py

2.2 运行时不出现dos命令窗口

在将带有tkinter等界面打包成exe时,运行exe文件的时候,会弹出一个dos命令窗口,这个窗口可以看到一些打印信息,如果想只运行tkinter 页面,去掉dos命令窗口,需要在打包的时候 加上: -w
例如:

pyinstaller -F test.py -w

编译完成后,一般会在当前文件夹中生成一个dist的文件夹,编译后的exe文件就在该文件夹中。

3. 编译后exe文件太大问题

3.1 可能遇到的问题

很多时候,我们的Python脚本文件本身是很小的,才几十KB,但编译后达到几十MB甚至上百MB,主要原因是在编译时会把Python环境及库一起打包到exe文件中,如果我们的Python环境中安装了很多包,比如通过Anaconda安装的Python环境时,通过会安装了很多Python库,如果在此环境中进行编译,那么就会把所有的库带上,导致编译后的exe文件很大。

3.2 解决办法

为该文件单独创建相应的Python虚拟环境,只安装要编译Python文件所依赖的库(导入到该Python文件中的库),在该虚拟环境中安装pyinstaller编译工具(这点很重要),然后运行该虚拟环境,在虚拟环境下执行编译,这时文件就会小很多。
亲测十分有效,单独创建Python虚拟环境后编译,exe文件大小由原来的95MB减少到16MB,运行速度也快了10倍,原来界面加载出来要40~50秒,现在只要不到5秒。

3.3 Windows虚拟环境创建方法

安装virtualenv:

pip install virtualenv

然后创建一个虚拟环境:

virtualenv py2exe_env  # 命名请自定义

创建完成以后,我们就会在创建的文件夹里发现虚拟环境命名的文件夹py2exe_env:

在这里插入图片描述

然后cd 到虚拟环境py2exe_env的Script目录下,输入如下命令启动虚拟环境:

activate py2exe_env

就可以在该虚拟环境中通过pip安装必要模块,注意:别忘了重新安装pyinstaller !

安装完成后,在该虚拟环境中(虚拟环境启动状态下),cd到要编译文件目录下,输入编译命令:

pyinstaller -F test.py -w  # 运行时不出现dos命令窗口

或者:

pyinstaller -F test.py  # 运行时出现dos命令窗口

就可以将Python代码文件编译成较小的exe文件啦!

4. 打包selenium脚本时集成chromedriver.exe的问题

采用pyinstaller 打包selenium项目时,一般需要再同级目录中添加谷歌浏览器chromedriver.exe驱动,否则会报错。在转发给其他人使用时,还要附带一个驱动,就会很不方便。通过如下方法可以解决此问题。

4.1 在代码中添加chromedriver.exe的路径

解决问题的关键,在代码中添加sys._MEIPASS路径,代码如下:

import sys


def getDriver():
    if getattr(sys, 'frozen', False):
        # 从exe包里找chromedriver依赖驱动的情况
        chromedriver_path = os.path.join(sys._MEIPASS, "chromedriver.exe")
        driver = webdriver.Chrome(chromedriver_path)
    else:
        # 普通情况下从本地文件路径找依赖的情况
        driver = webdriver.Chrome(executable_path='本地chromedriver的路径')  # 修改为自己电脑中chromedriver的路径
    return  driver


if __name__ == '__main__':
    driver = getDriver()
	driver.get('https://www.baidu.com')
	#无论是本地调试还是打包成exe都不会再报错了。	

注意:用 pyinstaller 打包生成的 exe 文件,在运行时动态生成依赖文件,sys._MEIPASS就是这些依赖文件所在文件夹的路径,通常为 C:\Windows\Temp_MEIxxxx 或C:\Users\用户名\AppData\Local\Temp_MEIxxxx,仅在 exe运行时有效,IDE运行时报错,因此需要通过判定条件兼顾。

4.2 打包时把chromedriver.exe添加进去

用pyinstaller打包时,把chromedriver.exe添加进去,重新打包,然后就可以使用了,打包命令如下:

pyinstaller -F --add-binary "chromedriver.exe";"."  test.py  # 把文件名换成你的文件名

打包完成后:,sepc文件中会出现添加了chromedriver.exe驱动,如下图所示:
在这里插入图片描述

数据绑定 - 在 .NET 中实施数据绑定的更好方法 | Microsoft Learn

数据绑定 - 在 .NET 中实施数据绑定的更好方法

作者 Mark Sowul

数据绑定是一种开发 UI 的有效技术: 数据绑定可以更轻松地区分视图逻辑和业务逻辑,而且还可以更简便地测试生成的代码。虽然从一开始数据绑定在 Microsoft .NET Framework 中就一直存在,但它是随着 Windows Presentation Foundation (WPF) 和 XAML 的产生而日趋重要的,因为在“模型-视图-视图模型”(MVVM) 模式中,数据绑定充当着“视图”和“视图模型”的“粘合剂”。

一直以来,魔幻字符串和样本代码要广播属性的更改及绑定 UI 元素时,都需要处理实施数据绑定中的问题。近年来,出现了各种工具包和技术来降低数据绑定的难度。本文旨在进一步简化数据绑定的流程。

首先,我将回顾实施数据绑定的基本知识及简化流程的通用技术(如果对本主题已有所了解,请自行跳过相关章节)。之后,我将开发一种你之前可能从未想过的技术(“第三种方法”),并介绍对使用 MVVM 开发应用程序时遇到的相关设计难题的解决方案。你可以在附带的“代码下载”中获取我在此处开发的框架的最终版,或将 SolSoft.DataBinding NuGet 包添加到自己的项目中。

基础知识: INotifyPropertyChanged

实施 INotifyPropertyChanged 是启用要绑定到 UI 的对象的首选方法。此方法非常简单,只包括一个成员:PropertyChanged 事件。当可绑定的属性更改时,该对象会引发此事件,以通知视图应刷新属性值的表示。

相互作用的关系很简单,但实施过程不简单。使用硬解码的文本属性名称来手动引发事件不是一种很好的解决方案,也无法进行重构: 你必须仔细确保文本名称与代码中的属性名称保持同步。这一点会使后续人员对你有所不满。例如:

C#
public int UnreadItemCount
{
  get
  {
    return m_unreadItemCount;
  }
  set
  {
    m_unreadItemCount = value;
    OnNotifyPropertyChanged(
      new PropertyChangedEventArgs("UnreadItemCount")); // Yuck
  }
}

人们开发了多种技术来解决上述问题,以维持正常运行(例如,可以参阅 bit.ly/24ZQ7CY 中的“堆栈溢出”问题);其中大部分技术可以归为两种类型之一。

通用技术 1: 基类

简化问题的方法之一是使用基类,以重复使用样本逻辑中的一部分。这样还可以提供几种以编程方式获取属性名称的方法,而不必再对其进行硬编码。

使用表达式获取属性名称: .NET Framework 3.5 引入了表达式,可以对代码结构进行运行时检查。LINQ 使用此 API 发挥了巨大的作用,例如,将 .NET LINQ 查询转换为 SQL 语句。勇于创新的开发人员还利用此 API 来检查属性名称。使用基类来执行此检查时,上述资源库可以重新编写为:

C#
public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(() => UnreadItemCount);
}

这样,重命名 UnreadItemCount 也会重命名表达式引用,这样代码仍然有效。RaiseNotifyPropertyChanged 的签名将如下所示:

C#
void RaiseNotifyPropertyChanged<T>(Expression<Func<T>> memberExpression)

存在各种从 memberExpression 中检索属性名称的技术。bit.ly/25baMHM 中的 C# MSDN 博客提供了一个简单示例:

C#
public static string GetName<T>(Expression<Func<T>> e)
{
  var member = (MemberExpression)e.Body;
  return member.Member.Name;
}

StackOverflow 在 bit.ly/23Xczu2 中展示了更为详细的列表。任何情况下,此技术都存在不足: 检索表达式名称使用反射,而反射很缓慢。根据属性更改通知的数量,性能开销可能很大。

使用 CallerMemberName 获取属性名称: C# 5.0 和 .NET Framework 4.5 带来了另外一种检索属性名称的方法,即使用 CallerMemberName 属性(你可以通过 Microsoft.Bcl NuGet 包中的 .NET Framework 的较旧版本来使用此属性)。此时,编译器负责所有工作,所以不存在运行时开销。通过此方式,对应的方法变成:

C#
void RaiseNotifyPropertyChanged<T>([CallerMemberName] string propertyName = "")
And the call to it is:
public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged();
}

属性指示编译器填充调用者名称、UnreadItemCount,来作为可选参数 propertyName 的值。

使用 nameof 获取属性名称: CallerMemberName 属性可能是为此使用案例(在基类中引发 PropertyChanged)量身定制的,但是在 C# 6 中,编译器团队最后提供了用途更为广泛的方法,即 nameof 关键字。nameof 方便用于多种用途;在此案例中,如果我用 nameof 替代了基于表达式的代码,则编译器将再一次负责所有工作(且不存在运行时开销)。需要注意的是,这完全属于编译器版本特性,而非 .NET 版本特性: 你可以使用此技术,且仍使用 .NET Framework 2.0。然而,你(及团队所有成员)必须至少在使用 Visual Studio 2015。使用 nameof 的过程如下:

C#
public int UnreadItemCount
...
set
{
  m_unreadItemCount = value;
  RaiseNotifyPropertyChanged(nameof(UnreadItemCount));
}

但是所有基类技术都存在一个普遍的问题: 正如大家所言,基类技术“会消耗你的基类”。如果你希望视图模型扩展不同的类,那么你的希望会落空。基类技术也不会处理“依赖”属性(例如,连接 FirstName 和 LastName 的 FullName 属性): 对 FirstName 或 LastName 的任何更改还必须触发在 FullName 上的更改。

通用技术 2: 面向方面的编程

面向方面的编程 (AOP) 是一种基本上在运行时或编译后对已编译的代码进行后续处理以添加某些行为(称为“方面”)的技术。通常以替换重复的样本代码为目的,例如日志记录或异常处理(即所谓的“横切关注点)。毫无意外,实施 INotifyPropertyChanged 是个不错的选择。

此方法有多个工具包可用。PostSharp 是其中之一 (bit.ly/1Xmq4n2)。我意外而惊喜地发现 PostSharp 恰当地处理了依赖属性(例如,前文中提到的 FullName 属性)。称为“Fody”的开源框架与此相似 (bit.ly/1wXR2VA)。

这是一种很吸引人的方法,其不足几乎不值一提。一些实施方案会在运行时拦截行为,从而导致性能成本。而编译后框架则截然不同,它不会产生任何运行时开销,但可能需要进行一些安装或配置。PostSharp 目前作为 Visual Studio 的扩展提供。其免费的 Express 版本仅限将 INotifyPropertyChanged 方面用于 10 个类,因此这也似乎意味着一笔金钱支出。另一外面,Fody 是一款免费的 NuGet 包,这也使其看似是一种诱人的选择。无论如何,请记住使用任何 AOP 框架编写的代码与你将运行和调试的代码并不完全相同。

第三种方法

处理上述状况的备选方法是利用面向对象的设计: 让属性自己负责引发事件! 虽然这不是一个非常创新的计划,但却是我在自己的项目外没有见到过的想法。基本来说,此方法类似以下内容:

C#
public class NotifyProperty<T>
{
  public NotifyProperty(INotifyPropertyChanged owner, string name, T initialValue);
  public string Name { get; }
  public T Value { get; }
  public void SetValue(T newValue);
}

计划是你提供一个具有名称和对所有者的引用的属性,然后让此属性负责引发 PropertyChanged 事件 - 类似以下:

C#
public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.PropertyChanged(m_owner, new PropertyChangedEventArgs(Name));
  }
}

问题在于这个过程无法真正实施: 我无法以此方式从其他类中引发事件。我需要与所属类建立一些协定来允许我引发 PropertyChanged 事件:这完全属于接口的工作范围,因此我将创建一个接口:

C#
public interface IRaisePropertyChanged
{
  void RaisePropertyChanged(string propertyName)
}

具备此接口后,我就可以真正实施 Notify­Property.SetValue 了:

C#
public void SetValue(T newValue)
{
  if(newValue != m_value)
  {
    m_value = newValue;
    m_owner.RaisePropertyChanged(this.Name);
  }
}

实施 IRaisePropertyChanged: 需要属性所有者来实施接口即代表每个视图模型类都需要图 1 中所示的一些样本。第一部分是所有类实施 INotifyPropertyChanged 必备的;第二部分专门针对新的 IRaisePropertyChanged。注意:由于 RaisePropertyChanged 方法不用于普通用途,因此我倾向于将实施过程清楚地呈现出来。

图 1 实施 IRaisePropertyChanged 需要的代码

C#
// PART 1: required for any class that implements INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
  // In C# 6, you can use PropertyChanged?.Invoke.
  // Otherwise I'd suggest an extension method.
  var toRaise = PropertyChanged;
  if (toRaise != null)
    toRaise(this, args);
}
// PART 2: IRaisePropertyChanged-specific
protected virtual void RaisePropertyChanged(string propertyName)
{
  OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
// This method is only really for the sake of the interface,
// not for general usage, so I implement it explicitly.
void IRaisePropertyChanged.RaisePropertyChanged(string propertyName)
{
  this.RaisePropertyChanged(propertyName);
}

我可以将此样本放入基类并进行扩展,这好像又回到了前面的讨论。毕竟,如果我将 CallerMemberName 应用到 RaisePropertyChanged 方法,那么我基本上是重新设计了第一种技术,那有什么意义呢? 在这两种情况下,如果其他类无法从基类中派生,我都只能将样本复制到其他类。

与前面基类技术相比,关键区别在于此时在样本中没有真正的逻辑;所有逻辑都封装在 NotifyProperty 类。在引发事件前检查属性值是否已更改属于简单逻辑,但最好不要复制此逻辑。如果想要使用一个不同的 IEqualityComparer 来执行此检查,请考虑一下会发生什么。使用此模型,你只需要改变 NotifyProperty 类。即使你有多个具有相同 IRaisePropertyChanged 样本的类,每个实施方案无需更改自身的任何代码即可从对 NotifyProperty 的更改中获得需要的内容。无论你想要引入何种行为更改,IRaisePropertyChanged 代码都不会更改。

整理汇总: 现在我有了视图模型需要实施的接口和用于进行数据绑定的属性的 NotifyProperty 类。最后一步是构造 NotifyProperty;为此,你仍需要以某种方式传入一个属性名。如果你恰好在使用 C# 6,就可以轻松地通过 nameof 运算符实现这一点。如果不是,你可以借助表达式创建 NotifyProperty,例如通过使用扩展方法(很不幸,这次 Caller­MemberName 无法发挥作用):

C#
public static NotifyProperty<T> CreateNotifyProperty<T>(
  this IRaisePropertyChanged owner,
  Expression<Func<T>> nameExpression, T initialValue)
{
  return new NotifyProperty<T>(owner,
    ObjectNamingExtensions.GetName(nameExpression),
    initialValue);
}
// Listing of GetName provided earlier

通过这种方式,你仍将支付反射成本,但仅限于在创建对象时支付,而不是每次属性更改时都支付。如果(你在创建许多对象时)这仍然太过昂贵,你可以总是缓存对 GetName 的调用,并将其保存为视图模型类中的一个静态只读值。图 2 表示的是以上两种情况中的简单视图模型的示例。

图 2 具有 NotifyProperty 的基本视图模型

C#
public class LogInViewModel : IRaisePropertyChanged
{
  public LogInViewModel()
  {
    // C# 6
    this.m_userNameProperty = new NotifyProperty<string>(
      this, nameof(UserName), null);
    // Extension method using expressions
    this.m_userNameProperty = this.CreateNotifyProperty(() => UserName, null);
  }
  private readonly NotifyProperty<string> m_userNameProperty;
  public string UserName
  {
    get
    {
      return m_userNameProperty.Value;
    }
    set
    {
      m_userNameProperty.SetValue(value);
    }
  }
  // Plus the IRaisePropertyChanged code in Figure 1 (otherwise, use a base class)
}

绑定和重命名:在讨论名称时,也是探讨其他数据绑定问题的好时机。安全引发不含硬编码的字符串的 PropertyChanged 事件表示重构成功了一半;数据绑定自身则是另一半。如果你对用于 XAML 中绑定的属性进行重命名,我会说,这未必成功(例如,可以参阅 bit.ly/1WCWE5m)。

备选做法是在代码隐藏文件中手动编码数据绑定。例如,

C#
// Constructor
public LogInDialog()
{
  InitializeComponent();
  LogInViewModel forNaming = null;
  m_textBoxUserName.SetBinding(TextBox.TextProperty,
    ObjectNamingExtensions.GetName(() => forNaming.UserName);
  // Or with C# 6, just nameof(LogInViewModel.UserName)
}

只为使用表达式功能而保留空对象显得有些奇怪,但这确实有效(如果你有 nameof 的权限,则无需如此)。

我认为此技术有一定价值,但我也承认其中的利弊。从好的方面来说,如果我重命名 UserName 属性,我可以很自信地保证重构会成功。另外一个巨大的好处则是“查找所有引用”按预期起作用。

不足的一方面则是没有像在 XAML 中进行绑定那样尽可能地简便和自然,并且使我无法保持 UI 设计的“独立性”。 例如,我无法仅仅在“混合”工具中重新设计外观而不必更改代码。此外,此技术不适用于数据模板;你可以将该模板提取到自定义控件,但需要耗费更多精力。

总的来说,我获得了更改“数据模型”端的灵活性,却以在“视图”端失去灵活性为代价。总之,由你决定是否优势取胜,并决定使用此方法进行绑定。

“派生”属性

前面我介绍了一种在其中引发 PropertyChanged 事件极为不便的场景,即为值依赖于其他属性的属性引发 PropertyChanged 事件时。我提到了一个简单示例,即依赖于 FirstName 和 LastName 的 FullName 属性。我实施此场景的目的在于传入基本 NotifyProperty 对象(FirstName 和 LastName)及计算其派生值的函数(例如,FirstName.Value + " " + LastName.Value),然后基于上述,生成将自动为我处理剩下部分的属性对象。为了实现上述目的,我将对最初的 NotifyProperty 进行一些调整。

第一个任务是在 NotifyProperty 上公开一个单独的 ValueChanged 事件。派生属性将在其基本属性中侦听此事件,并通过计算出一个新值进行回应(并为自身引发正确的 PropertyChanged 事件)。第二个任务是提取一个接口,即 IProperty<T>,来封装通用的 NotifyProperty 功能。此外,这允许我从其他派生的属性中获取派生的属性。生成的接口很直观,并列于此处(对 NotifyProperty 的相应更改非常简单,因此不会列出):

C#
public interface IProperty<TValue>
{
  string Name { get; }
  event EventHandler<ValueChangedEventArgs> ValueChanged;
  TValue Value { get; }
}

在你开始尝试将步骤综合前,创建 DerivedNotifyProperty 类似乎也比较简单。基本计划是传入基本属性和计算其一些新值的函数,但由于是泛型此过程立即陷入困境。没有传入多种不同属性类型的实际方法:

C#
// Attempted constructor
public DerivedNotifyProperty(IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

我可以通过使用静态的创建方法解决问题的前半部分(接受多种泛型类型):

C#
static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)

但是派生的属性仍需要侦听每个基础属性上的 ValueChanged 事件。解决此问题需要两个步骤。首先,我将 ValueChanged 事件提取到一个单独的接口中:

C#
public interface INotifyValueChanged // No generic type!
{
  event EventHandler<ValueChangedEventArgs> ValueChanged;
}
public interface IProperty<TValue> : INotifyValueChanged
{
  string Name { get; }
  TValue Value { get; }
}

这将允许 DerivedNotifyProperty 传入非泛型INotifyValueChanged 而不是泛型 IProperty<T>。其次,我需要计算不含泛型的新值: 我将使用接受两个泛型参数的原始 derivedValueFunction,并由此创建新的无需任何参数的匿名函数 - 新函数将引用两个属性传入的值。换言之,我将创建一个闭包。你可以在下列代码中查看此过程:

C#
static DerivedNotifyProperty<TDerived> CreateDerivedNotifyProperty
  <T1, T2, TDerived>(this IRaisePropertyChanged owner,
  string propertyName, IProperty<T1> property1, IProperty<T2> property2,
  Func<T1, T2, TDerived> derivedValueFunction)
{
  // Closure
  Func<TDerived> newDerivedValueFunction =
    () => derivedValueFunction (property1.Value, property2.Value);
  return new DerivedNotifyProperty<TValue>(owner, propertyName,
    newDerivedValueFunction, property1, property2);
}

新的“派生值”函数仅是没有任何参数的 Func<TDerived>;现在 DerivedNotifyProperty 无需基本属性类型的任何内容,所以我可以愉快地从多个不同类型的属性中创建一个 DerivedNotifyProperty。

另一个要点在于何时真正调用派生值函数。一种明显的实施方案是侦听每个基本属性的 ValueChanged 事件,一旦属性更改则调用函数。但在同一个操作中多个基本属性更改时(想象一下“重置”按钮清除窗体时),此方法效率并不高。一种更好的方法是根据需要生成值(并进行缓存),如果任何基本属性更改则使该值无效。Lazy<T> 是实施此过程的最好方法。

你可以在图 3 中查看 DerivedNotifyProperty 类的缩写列表。注意:虽然我仅列出了两种基本属性的创建方法,但此类可以传入任意数量的要侦听的属性。我创建其他的重载以传入一个基本属性、三个基本属性,以此类推。

图 3 DerivedNotifyProperty的核心实施

C#
public class DerivedNotifyProperty<TValue> : IProperty<TValue>
{
  private readonly IRaisePropertyChanged m_owner;
  private readonly Func<TValue> m_getValueProperty;
  public DerivedNotifyProperty(IRaisePropertyChanged owner,
    string derivedPropertyName, Func<TValue> getDerivedPropertyValue,
    params INotifyValueChanged[] valueChangesToListenFor)
  {
    this.m_owner = owner;
    this.Name = derivedPropertyName;
    this.m_getValueProperty = getDerivedPropertyValue;
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    foreach (INotifyValueChanged valueChangeToListenFor in valueChangesToListenFor)
      valueChangeToListenFor.ValueChanged += (sender, e) => RefreshProperty();
  }
  // Name property and ValueChanged event omitted for brevity 
  private Lazy<TValue> m_value;
  public TValue Value
  {
    get
    {
      return m_value.Value;
    }
  }
  public void RefreshProperty()
  {
    // Ensure we retrieve the value anew the next time it is requested
    this.m_value = new Lazy<TValue>(m_getValueProperty);
    OnValueChanged(new ValueChangedEventArgs());
    m_owner.RaisePropertyChanged(Name);
  }
}

注意:基本属性可能来自不同的所有者。例如,假定你具有一个包含 IsAddressValid 属性的“地址”视图模型。你还具有一个“订单”视图模型,其中含有两个用于账单地址和送货地址的“地址”视图模型。在父级“订单”视图模型上创建 IsOrderValid 属性来合并子“地址”视图模型中的 IsAddressValid 属性是明智的做法,这样只有在两个地址都有效时你才能提交订单。若要实现此目的,“地址”视图模型要公开 IsAddressValid { get; } 和 IProperty<bool> IsAddressValidProperty { get; } 两个布尔值,这样“订单”视图模型就可以创建一个 DerivedNotifyProperty 来引用子 IsAddressValidProperty 对象。

DerivedNotifyProperty 的实用性

我提供给派生属性的全名示例很大一部分是人为设计的,但我十分希望讨论一些真正的使用案例,并将案例与某些设计原则相结合。我仅在介绍一种示例: IsValid。这是一种相当简单而强大的方法,例如,可以禁用窗体的“保存”按钮。注意:不会限制你只将此技术用于 UI 视图模型的上下文中。你还可以使用此技术来验证业务对象;业务对象只需实施 IRaisePropertyChanged。

派生属性用途非常广泛的第二种情况是在“深化”场景中。列举一个简单的示例 - 想一想用于选择国家/地区的组合框,在其中选择一个国家/地区就会填充一系列的城市。你可以将 SelectedCountry 作为 NotifyProperty,并且在给定 GetCitiesForCountry 方法的情况下创建 AvailableCities 来作为在所选国家/地区更改时将自动保持同步的 DerivedNotifyProperty。

我使用 NotifyProperty 对象的第三种场合是用于指示对象是否“忙碌”。 如果对象被视为忙碌,会禁用某些 UI 功能,并且或许用户可以看到一个进度指示器。这是一个看似简单的场景,但是其中有许多需要指明的要点。

第一部分是跟踪对象是否忙碌;在简单的案例中,我可以使用布尔 NotifyProperty 来实现这个目的。但是,经常会发生的是对象可能出于多种原因中的一种而“忙碌”:例如,我正在加载多个区域的数据,有可能是并行执行。总体“忙碌”的状态应取决于是否有以上项中的任意项仍正在进行。这听起来几乎像是派生属性的工作范围,但使用派生属性进行这项工作会很不便(如果可能的话): 我需要为每个可能的操作设置各自的一个属性来追踪各个操作是否正在进行。不过,我想要使用单个 IsBusy 属性来对每个操作执行类似以下的内容:

C#
try
{
  IsBusy.SetValue(true);
  await LongRunningOperation();
}
finally
{
  IsBusy.SetValue(false);
}

为了实现此目的,我创建了 IsBusyNotifyProperty 类来扩展 NotifyProperty<bool>,并在其中保留了“忙碌计数”。 我重写了 SetValue,这样 SetValue(true) 会增加计数,而 Set­Value(false) 会减少计数。计数从 0 到 1 进行时,我调用 base.SetValue(true),而计数从 1 到 0时,则调用 base.SetValue(false)。使用这种方法时,启动多个未完成的操作仅会导致 IsBusy 成为 True 一次,之后在只有所有操作都完成后才会再次成为 False。你可以在代码下载中查看此实施过程。

以下就处理了事务“忙碌”端的问题: 我可以将“忙碌”绑定到进度指示器的可见性。要禁用 UI,则需进行相反的设置。当“忙碌”为 True 时,“UI 已启用”应为 False。

XAML 中具有 IValueConverter 这一概念,它可以将值和显示方式相互转换。BooleanToVisibilityConverter 是一个通用的示例 - 在 XAML 中,一个元素的“可见性”不由布尔表示,而由枚举值表示。这就表示无法将元素的可见性直接绑定到布尔属性(例如,IsBusy);你需要绑定该值,还需要使用一个转换器。例如,

XML
<StackPanel Visibility="{Binding IsBusy,
  Converter={StaticResource BooleanToVisibilityConverter}}" />

我提到过“启用 UI”是“忙碌”的对立面;创建一个值转换器来反转一个布尔属性并使用它执行操作,这个想法让人跃跃欲试:

XML
<Grid IsEnabled="{Binding IsBusy,
   Converter={StaticResource BooleanToInverseConverter}}" />

事实上,在我创建 DerivedNotifyProperty 类之前,这是最简单的方法。创建一个单独的属性,将其绑定到 IsBusy 的反面,然后引发正确的 PropertyChanged 事件,这个过程相当枯燥。但是现在变得轻松,而且少了这种人为障碍(即惰性),我更明确地意识到将 IValueConverter 用于何处更有意义。

最终,无论视图(例如,WPF 或 Windows 窗体,甚至一款控制台应用都是一种类型视图)是如何实现的,都应是对基础应用程序中的现状的一种可视化(或“投影”),而无需决定正在发生的事务的机制和商业规则。上述情况中,IsBusy 和 IsEnabled 恰好彼此紧密关联,这个状况属于实施细节;并非禁用 UI 就必定要专门关联到应用程序忙碌与否。

目前为止,我认为此方法尚不成熟,如果你想要使用值转换器来实施此步骤,我绝无异议。但是,我可以通过向此示例添加其他部分来构造一个更为严谨的案例。假设应用程序丢失了网络访问权限,其还要禁用 UI(并显示指示状况的面板)。那么,这可以分为三种情况: 如果应用程序忙碌,我要禁用 UI(并显示进度面板)。如果应用程序丢失了网络访问权限,我也要禁用 UI(并显示“丢失连接”面板)。第三种情况是应用程序处于连接状态,且并不忙碌,此时要准备接受输入。

在没有单独的 IsEnabled 属性的情况下尝试实施过程,最好的状态下也很不便;你可以使用 MultiBinding,但是这仍然很吃力,并且它并非在所有环境中都受支持。最终,这种不便通常表示还存在更好的方法,而现在我们知道确实存在:此逻辑在视图模型中更易处理。现在很轻松地即可将两个 NotifyProperties,即 IsBusy 和 IsDisconnected 公开,然后创建一个 DerivedNotifyProperty,即 IsEnabled,而只有以上两者都为 False 时,IsEnabled 才为 True。

如果你使用了 IValueConverter,并将 UI 的“启用”状态直接绑定到了 IsBusy(使用转换器来反转),现在你将需要进行很多处理了。相反,如果你公开了一个单独的派生的 IsEnabled 属性,添加这条新逻辑工作量要小的多,而且 IsEnabled 绑定自身甚至不需要更改。这是个好的预示,表明你的操作正确。

总结

布置此框架是个漫长的旅程,但获得的回报是现在我无需重复的样本、魔幻字符串,即可实施属性更改通知,还可以支持重构。我的视图模型不需要来自特定基类的逻辑。我可以创建派生的属性,而该属性无需进行额外的处理也可以引发正确的更改通知。最后,我看到的代码即是正在运行的代码。而我通过使用面向对象的设计开发了一个相当简单的框架,就实现了这一切。希望这对你自己的项目有所帮助。


Fody,告别烦人的INotifyPropertyChanged,最简方式实现通知! - OneByOneDotNet - 博客园 (cnblogs.com)

INotifyPropertyChanged

我不是针对谁,我是说在座的各位

相信所有学wpf的,都写过类似下面的代码:
实现INotifyPropertyChanged

public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

调用


    private string _userName = string.Empty;
    /// <summary>
    ///     用户名
    /// </summary>
    public string UserName
    {
      get => _userName;
      set
    {
      _userName = value;
      OnPropertyChanged();
    }
  }

当属性多起来时,这就很烦人了····
于是乎,我们的PropertyChanged.Fody就登场了

通过nuget安装PropertyChanged.Fody

这是一个附加组件库。我们可以通过nuget安装,也可以通过在程序包管理控制台输入以下内容:

PM> Install-Package Fody
PM> Install-Package PropertyChanged.Fody

手动添加FodyWeavers.xml文件

安装完成后,我们需要手动添加名为FodyWeavers.xml的文件,右键项目添加项选择xml文件即可。
注:该文件是Fody配置文件,更多信息请参考配置
如果仅仅实现通知,我们只需要在文件内添加一下内容

<Weavers>
  <PropertyChanged/>
</Weavers>

完成以上操作后,所有实现 INotifyPropertyChanged 的类都将通知代码注入到属性设置器中。
例如:

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }
    public string FullName => $"{GivenNames} {FamilyName}";
}

在编译后就会成为:

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    string givenNames;
    public string GivenNames
    {
        get => givenNames;
        set
        {
            if (value != givenNames)
            {
                givenNames = value;
                OnPropertyChanged(InternalEventArgsCache.GivenNames);
                OnPropertyChanged(InternalEventArgsCache.FullName);
            }
        }
    }

    string familyName;
    public string FamilyName
    {
        get => familyName;
        set 
        {
            if (value != familyName)
            {
                familyName = value;
                OnPropertyChanged(InternalEventArgsCache.FamilyName);
                OnPropertyChanged(InternalEventArgsCache.FullName);
            }
        }
    }

    public string FullName => $"{GivenNames} {FamilyName}";

    protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
    {
        PropertyChanged?.Invoke(this, eventArgs);
    }
}

internal static class InternalEventArgsCache
{
    internal static PropertyChangedEventArgs FamilyName = new PropertyChangedEventArgs("FamilyName");
    internal static PropertyChangedEventArgs FullName = new PropertyChangedEventArgs("FullName");
    internal static PropertyChangedEventArgs GivenNames = new PropertyChangedEventArgs("GivenNames");
}

特性

我们自然有些特殊需求,例如我需要更新A属性通知B属性,需要某些属性不通知等等需求。于是Fody给我们提供了标记特性。

AlsoNotifyForAttribute(允许注入指向不同属性的通知代码。)

我们只需要在属性上打上要通知的属性即可。

public class Person : INotifyPropertyChanged
{
    [AlsoNotifyFor("FullName")]
    public string GivenName { get; set; }

    [AlsoNotifyFor("FullName")]
    public string FamilyName { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    public string FullName { get; set; }
}

DoNotNotifyAttribute(不要通知我)

我们也可以标记某属性更新时不需要通知。

public class Person : INotifyPropertyChanged
{
    public string GivenName { get; set; }
    [DoNotNotify]
    public string FamilyName { get; set; }
    public event PropertyChangedEventHandler PropertyChanged;
}

DependsOnAttribute(注入此属性以便在设置依赖属性时得到通知。)

public class Person : INotifyPropertyChanged
{
    public string GivenName { get; set; }

    public string FamilyName { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    [DependsOn("GivenName","FamilyName")]
    public string FullName { get; set; }
}

本人不是大佬,只是道路先行者,在落河后,向后来的人大喊一声,这里有坑,不要过来啊!

纵然如此,依旧有人重复着落河,重复着呐喊······

C# 简化通知接口(INotifyPropertyChanging, INotifyPropertyChanged)_c# inotifychanged_kurame的博客-CSDN博客

原来类继承通知接口的写法过于复杂

最基础的类:

//最原始的基础类
class Country
{
	public string Code { get; set; }
	public string Name { get; set; }
}

继承接口后的写法:

//继承通知接口后的写法
class Country : INotifyPropertyChanging, INotifyPropertyChanged
{
	public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyChanged(string propertyName)
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    private void NotifyChanging(string propertyName)
            => PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));

	private string code;
	public string Code 
	{ 
		get => code; 
		set
		{
			NotifyChanging("Code");
			code = value;
			NotifyChanged("Code");
		}
	}
	private string name;
	public string Name
	{ 
		get => name; 
		set
		{
			NotifyChanging("Name");
			name = value;
			NotifyChanged("Name");
		}
	}
}

进行对比发现多了很多代码,而且还额外多了一个private的字段,这样调试也会重复显示相同的数据,改一个名字至少要修改6个地方。

这个问题可以写个抽象类进行解决

//通知抽象类
public abstract class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged
{
	//储存属性值的字典
	//new() 是C# 9.0 的语法糖
	private Dictionary<string, object> property = new();
	/// <summary>
    /// 设置值时触发通知事件
    /// </summary>
    /// <param name="value">需要设置的值</param>
    /// <param name="propertyName">CallerMemberName属性可以获取调用方的名称(不需要手动设置)</param>
	protected void SetValueWithNotify(object value, [CallerMemberName] string propertyName = "")
	{
		NotifyChanging(propertyName);
        if (property.ContainsKey(propertyName))
        {
        	property[propertyName] = value;
        }
        else
        {
        	property.Add(propertyName, value);
        }
        NotifyChanged(propertyName);
    }
	/// <summary>
    /// 获得对应的值
    /// </summary>
    /// <typeparam name="T">需要转换的类型</typeparam>
    /// <param name="propertyName">CallerMemberName属性可以获取调用方的名称(不需要手动设置)</param>
    protected T GetValue<T>([CallerMemberName] string propertyName = "")
    	=> property.ContainsKey(propertyName) ? (T)property[propertyName] : default;

	public event PropertyChangingEventHandler PropertyChanging;
	public event PropertyChangedEventHandler PropertyChanged;

	private void NotifyChanged(string propertyName)
		=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

	private void NotifyChanging(string propertyName)
		=> PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}

继承抽象类以后的代码:

class Country : NotifyingEntity 
{
	public string Code { get => GetValue<string>(); set => SetValueWithNotify(value); }
	public string Name { get => GetValue<string>(); set => SetValueWithNotify(value); }
}

简化后基本和原来一致,只需要注意获取值时候需要指定对应的类型。
利用VS的批量更改功能可以轻易的将{ get; set; } 替换成 { get => GetValue<>(); set => SetValueWithNotify(value); }
然后修改一下对应的类型就行了,更高级一点可以用正则去替换。

[Windows] Prism 8.0 入门(上):Prism.Core

1. Prism 简介

Prism 是一个用于构建松耦合、可维护和可测试的 XAML 应用的框架,它支持所有还活着的基于 XAML 的平台,包括 WPF、Xamarin Forms、WinUI 和 ~~Uwp~~ Uno。Prism 提供了一组设计模式的实现,这些模式有助于编写结构良好且可维护的 XAML 应用程序,包括 MVVM、依赖项注入、命令、事件聚合器等。

Prism 是一个有10年以上历史的框架,而上个月才刚发布了它的 8.0 版本,这意味着现在网上能找到的大部分 Prism 的资料都已经有点过时,连 官方文档 也不例外。如果你需要详细的文档,除了官方文档,我会推荐 RyzenAdorer 的 Prism 系列文章:

NET Core 3 WPF MVVM框架 Prism系列文章索引 - RyzenAdorer -

如果你不需要那么详细的文档,只需要一个入门的教程,那么我希望我写的这两篇文章可以帮到你。

2. Prism.Core、Prism.Wpf 和 Prism.Unity

从很久以前开始,臃肿 就是 Prism 被提起最多的标签。毕竟比起 MVVMLight,Prism 实现的功能更多;对于初学者来说,刚打开 Prism 的文档很可能会马上选择放弃。Prism 的文档详细到让人望而却步,例如多年前的旧版官方文档的 其中一篇

 

 

不是 6 分钟,不是 16 分账,是整整 60 分钟,Prism 的旧文档随便打开一篇都吓死人。而 Prism 的各种包更是多到离谱。例如几年前的 Prism 6.3,其中 WPF 平台的项目有这么多个:

  • Prism.Wpf
  • Prism.Autofac
  • Prism.DryIoc
  • Prism.Mef
  • Prism.Ninject
  • Prism.StructureMap
  • Prism.Unity

所以臃肿是很多人对 Prism 的印象。

减肥是一个永恒的受欢迎的话题,对 Prism 也是一样。相比 Prism 6.3,刚刚发布的 8.0 已经好很多了(虽然还是有很多个项目),例如 WPF 平台的项目已经大幅删减,只保留了 Prism.Wpf、Prism.DryIoc 和 Prism.Unity,也就是说现在 Prism 只支持 DryIoc 和 Unity 两种 IOC 容器。这样一来 Prism 项目的结构就很清晰了。

以 WPF 为例,核心的项目是 Prism.Core,它提供实现 MVVM 模式的核心功能以及部分各平台公用的类。然后是 Prism.Wpf,它提供针对 Wpf 平台的功能,包括导航、弹框等。最后由 Prism.Unity 指定 Unity 作为 IOC 容器。

 

 

即使已精简了这么多,Prism 还是有很多功能,两篇文章也不足以讲解全部内容,所以我只会介绍最常用到的入门知识。这篇文章首先介绍 Prism.Core 的主要功能。

3. Prism.Core

Prism.Core 可以单独安装,目前最新的版本是 8.0.0.1909:

Install-Package Prism.Core -Version 8.0.0.1909

除了一些各个平台都用到的零零碎碎的公用类,作为一个 MVVM 库 Prism.Core 主要提供了下面三方面的功能:

  • BindableBase 和 ErrorsContainer
  • Commanding
  • Event Aggregator

这些功能已经覆盖了 MVVM 的核心功能,如果只需要与具体平台无关的 MVVM 功能,可以只安装 Prism.Core。

4. BindableBase 和 ErrorsContainer

数据绑定是 MVVM 的核心元素之一,为了使绑定的数据可以和 UI 交互,数据类型必须继承 INotifyPropertyChanged。 BindableBase 实现了 INotifyPropertyChanged 最简单的封装,它的使用如下:

public class MockViewModel : BindableBase
{
    private string _myProperty;
    public string MyProperty
    {
        get { return _myProperty; }
        set { SetProperty(ref _myProperty, value); }
    }
}

其中 SetProperty 判断 _myProperty 和 value 是否相等,如果不相等就为 _myProperty 赋值并触发 OnPropertyChanged 事件。

除了 INotifyPropertyChanged,绑定机制中另一个十分有用的接口是 INotifyDataErrorInfo,它用于公开数据验证的结果。Prism 提供了 ErrorsContainer 以便管理及通知数据验证的错误信息。要使用 ErrorsContainer,可以先写一个类似这样的基类:

public class DomainObject : BindableBase, INotifyDataErrorInfo
{
    public ErrorsContainer<string> _errorsContainer;

    protected ErrorsContainer<string> ErrorsContainer
    {
        get
        {
            if (_errorsContainer == null)
                _errorsContainer = new ErrorsContainer<string>(s => OnErrorsChanged(s));

            return _errorsContainer;
        }
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public void OnErrorsChanged(string propertyName)
    {
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }

    public IEnumerable GetErrors(string propertyName)
    {
        return ErrorsContainer.GetErrors(propertyName);
    }

    public bool HasErrors
    {
        get { return ErrorsContainer.HasErrors; }
    }
}

然后就可以在派生类中通过 ErrorsContainer.SetErrors 和 ErrorsContainer.ClearErrors 管理数据验证的错误信息:

public class MockValidatingViewModel : DomainObject
{
    private int mockProperty;

    public int MockProperty
    {
        get
        {
            return mockProperty;
        }

        set
        {
            SetProperty(ref mockProperty, value);

            if (mockProperty < 0)
                ErrorsContainer.SetErrors(() => MockProperty, new string[] { "value cannot be less than 0" });
            else
                ErrorsContainer.ClearErrors(() => MockProperty);
        }
    }
}

5. Commanding

ICommand 同样是 MVVM 模式的核心元素,DelegateCommand 实现了 ICommand 接口,它最基本的使用形式如下,其中 DelegateCommand 构造函数中的第二个参数 canExecuteMethod 是可选的:

public DelegateCommand SubmitCommand { get; private set; }

public CheckUserViewModel()
{
    SubmitCommand = new DelegateCommand(Submit, CanSubmit);
}

private void Submit()
{
    //implement logic
}

private bool CanSubmit()
{
    return true;
}

另外它还有泛型的版本:

public DelegateCommand<string> SubmitCommand { get; private set; }

public CheckUserViewModel()
{
    SubmitCommand = new DelegateCommand<string>(Submit, CanSubmit);
}

private void Submit(string parameter)
{
    //implement logic
}

private bool CanSubmit(string parameter)
{
    return true;
}

通常 UI 会根据 ICommand 的 CanExecute 函数的返回值来判断触发此 Command 的 UI 元素是否可用。CanExecute 返回 DelegateCommand 构造函数中的第二个参数 canExecuteMethod 的返回值。如果不传入这个参数,则 CanExecute 一直返回 True。

如果 CanExecute 的返回值有变化,可以调用 RaiseCanExecuteChanged 函数,它会触发 CanExecuteChanged 事件并通知 UI 元素重新判断绑定的 ICommand 是否可用。除了主动调用 RaiseCanExecuteChangedDelegateCommand 还可以用 ObservesProperty 和 ObservesCanExecute 两种形式监视属性,定于属性的 PropertyChanged 事件并改变 CanExecute

private bool _isEnabled;
public bool IsEnabled
{
    get { return _isEnabled; }
    set { SetProperty(ref _isEnabled, value); }
}

private bool _canSave;
public bool CanSave
{
    get { return _canSave; }
    set { SetProperty(ref _canSave, value); }
}


public CheckUserViewModel()
{
    SubmitCommand = new DelegateCommand(Submit, CanSubmit).ObservesProperty(() => IsEnabled);
    //也可以写成串联方式
    SubmitCommand = new DelegateCommand(Submit, CanSubmit).ObservesProperty(() => IsEnabled).ObservesProperty<bool>(() => CanSave);

    SubmitCommand = new DelegateCommand(Submit).ObservesCanExecute(() => IsEnabled);
}

6. Event Aggregator

本来Event Aggregator(事件聚合器)或 Messenger 之类的组件本来并不是 MVVM 的一部分,不过现在也成了 MVVM 框架的一个重要元素。解耦是 MVVM 的一个重要目标,'EventAggregator' 则是实现解耦的重要工具。在 MVVM 中,对于 View 和与他匹配的 ViewModel 之间的交互,可以使用 INotifyProperty 和 Icommand;而对于必须通信的不同 ViewModel 或模块,为了使它们之间实现低耦合,可以使用 Prism 中的 EventAggregator。如下图所示,Publisher 和 Scbscriber 之间没有直接关联,它们通过 Event Aggregator 获取 PubSubEvent 并发送及接收消息:

 

 

要使用 EventAggregator,首先需要定义 PubSubEvent

public class TickerSymbolSelectedEvent : PubSubEvent<string>{}

发布方和订阅方都通过 EventAggregator 索取 PubSubEvent,在 ViewModel中通常都是通过依赖注入获取一个 IEventAggregator

public class MainPageViewModel
{
    IEventAggregator _eventAggregator;
    public MainPageViewModel(IEventAggregator ea)
    {
        _eventAggregator = ea;
    }
}

发送方的操作很简单,只需要 通过 GetEvent 拿到 PubSubEvent,把消息发布出去,然后拍拍屁股走人,其它的责任都不用管:

_eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish("STOCK0");

订阅方是真正使用这些消息并负责任的人,下面是最简单的通过 Subscribe 订阅事件的代码:

public class MainPageViewModel
{
    public MainPageViewModel(IEventAggregator ea)
    {
        ea.GetEvent<TickerSymbolSelectedEvent>().Subscribe(ShowNews);
    }

    void ShowNews(string companySymbol)
    {
        //implement logic
    }
}

除了基本的调用方式,Subscribe 函数还有其它可选的参数:

public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive)

其中 threadOption 指示收到消息后在哪个线程上执行第一个参数定义的 action,它有三个选项:

  • PublisherThread,和发布者保持在同一个线程上执行。
  • UIThread,在 UI 线程上执行。
  • BackgroundThread,在后台线程上执行。

第三个参数 keepSubscriberReferenceAlive 默认为 false,它指示该订阅是否为强引用。

  • 设置为 false 时,引用为弱引用,用完可以不用管。
  • 设置为 true 时,引用为强引用,用完需要使用 Unsubscribe 取消订阅。

下面代码是一段订阅及取消订阅的示例:

public class MainPageViewModel
{
    TickerSymbolSelectedEvent _event;

    public MainPageViewModel(IEventAggregator ea)
    {
        _event = ea.GetEvent<TickerSymbolSelectedEvent>();
        _event.Subscribe(ShowNews);
    }

    void Unsubscribe()
    {
        _event.Unsubscribe(ShowNews);
    }

    void ShowNews(string companySymbol)
    {
        //implement logic
    }
}

7. 生产力工具

如果觉得属性和 DelegateCommand 的定义有些啰嗦,可以试试安装这个工具:Prism Template Pack,它提供了一些实用的代码段和一些 Project 和 Item 的模板。例如,安装此工具后可以通过 cmd 代码段快速生成一个完整的 DelegateCommand 代码:

private DelegateCommand _fieldName;
public DelegateCommand CommandName =>
    _fieldName ?? (_fieldName = new DelegateCommand(ExecuteCommandName));

void ExecuteCommandName()
{

}

更多代码段定义请参考官方文档:Productivity Tools Prism

8. 结语

Prism.Core 最初由 Microsoft Patterns and Practices 团队创建,现在转移到社区。虽然 Prism 框架非常成熟(还有点臃肿),支持插件和定位控件的区域,但 Prism.Core 很轻,仅包含几个常用的类型。这篇文章已经把 Prism.Core 中最常用的类尽可能简单地介绍过一遍,这足够用完创建一个基于 MVVM 框架的项目。

 [Windows] Prism 8.0 入门(下):Prism.Wpf 和 Prism.Unity - 知乎 (zhihu.com)

1. Prism.Wpf 和 Prism.Unity

这篇是 Prism 8.0 入门的第二篇文章,上一篇介绍了 Prism.Core,这篇文章主要介绍 Prism.Wpf 和 Prism.Unity。

以前做 WPF 和 Silverlight/Xamarin 项目的时候,我有时会把 ViewModel 和 View 放在不同的项目,ViewModel 使用 可移植类库项目,这样 ViewModel 就与 UI 平台无关,实现了代码复用。这样做还可以强制 View 和 ViewModel 解耦。

现在,即使在只写 WPF 项目的情况下,但为了强制 ViewModel 和 View 假装是陌生人,做到不留后路,我也倾向于把 View 和 ViewModel 放到不同项目,并且 ViewModel 使用 .Net Standard 作为目标框架。我还会假装下个月 UWP 就要崛起了,我手头的 WPF 项目中的 ViewModel 要做到平台无关,方便我下个月把项目移植到 UWP 项目中。

但如果要使用 Prism 构建 MVVM 程序的话,上面这些根本不现实。首先,Prism 做不到平台无关,它针对不同的平台提供了不同的包,分别是:

  • 针对 WPF 的 Prism.Wpf
  • 针对 Xamarin Forms 的 Prism.Forms
  • 针对 Uno 平台的 Prism.Uno

其次,根本就没有针对 UWP 的 Prism.Windows(UWP 还有未来,忍住别哭)。

所以,除非只使用 Prism.Core,否则要将 ViewModel 项目共享给多个平台有点困难,毕竟用在 WPF 项目的 Prism.Wpf 本身就是个 Wpf 类库。

现在“编写平台无关的 ViewModel 项目”这个话题就与 Prism 无关了,再把 Prism.Unity 和 Prism.Wpf 选为代表(毕竟这个组合比其它组合下载量多些),这篇文章就只用它们作为 Prism 入门的学习对象。

 

 

Prism.Core、Prism.Wpf 和 Prism.Unity 的依赖关系如上所示。其中 Prism.Core 实现了 MVVM 的核心功能,它是一个与平台无关的项目。Prism.Wpf 里包含了 Dialog Service、Region、Module 和导航等几个模块,都是些用在 WPF 的功能。Prism.Unity 本身没几行代码,它表示为 Prism.Wpf 选择了 UnityContainer 作为 IOC 容器。(另外还有 Prism.DryIoc 可以选择,但从下载量看 Prism.Unity 是主流。)

就算只学习 Prism.Wpf,可它的模块很多,一篇文章实在塞不下。我选择了 Dialog Service 作为代表,因为它的实现思想和其它的差不多,而且弹窗还是 WPF 最常见的操作。这篇文章将通过以下内容讲解如何使用 Prism.Wpf 构建一个 WPF 程序:

  • PrismApplication
  • RegisterTypes
  • XAML ContainerProvider
  • ViewModelLocator
  • Dialog Service

Prism 的最新版本是 8.0.0.1909。由于 Prism.Unity 依赖 Prism.Wpf,所以只需安装 Prism.Unity:

Install-Package Prism.Unity -Version 8.0.0.1909

2. PrismApplication

安装好 Prism.Wpf 和 Prism.Unity 后,下一步要做的是将 App.xaml 的类型替换为 PrismApplication

<prism:PrismApplication x:Class="PrismTest.App"
                        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:prism="http://prismlibrary.com/">
    <Application.Resources>
    </Application.Resources>
</prism:PrismApplication>

上面是修改过的 App.xaml,将 Application 改为 prism:PrismApplication,并且移除了 StartupUri="MainWindow.xaml"

接下来不要忘记修改 App.xaml.cs:

public partial class App : PrismApplication
{
    public App()
    {
    }

    protected override Window CreateShell()
        => Container.Resolve<ShellWindow>();
}

PrismApplication 不使用 StartupUri ,而是使用 CreateShell 方法创建主窗口。CreateShell 是必须实现的抽象函数。PrismApplication 提供了 Container 属性,CreateShell 函数里通常使用 Container 创建主窗口。

3. RegisterTypes

其实在使用 CreateShell 函数前,首先必须实现另一个抽象函数 RegisterTypes。由于 Prism.Wpf 相当依赖于 IOC,所以要现在 PrismApplication 里注册必须的类型或依赖。PrismApplication 里已经预先注册了 DialogServiceEventAggregatorRegionManager 等必须的类型(在 RegisterRequiredTypes 函数里),其它类型可以在 RegisterTypes 里注册。它看起来像这样:

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    // Core Services

    // App Services

    // Views
    containerRegistry.RegisterForNavigation<BlankPage, BlankViewModel>(PageKeys.Blank);
    containerRegistry.RegisterForNavigation<MainPage, MainViewModel>(PageKeys.Main);
    containerRegistry.RegisterForNavigation<ShellWindow, ShellViewModel>();

    // Configuration
    var configuration = BuildConfiguration();

    // Register configurations to IoC
    containerRegistry.RegisterInstance<IConfiguration>(configuration);
}

4. XAML ContainerProvider

在 XAML 中直接实例化 ViewModel 并设置 DataContext 是 View 和 ViewModel 之间建立关联的最基本的方法:

<UserControl.DataContext>
    <viewmodels:MainViewModel/>
</UserControl.DataContext>

但现实中很难这样做,因为相当一部分 ViewModel 都会在构造函数中注入依赖,而 XAML 只能实例化具有无参数构造函数的类型。为了解决这个问题,Prism 提供了 ContainerProvider 这个工具,通过设置 Type 或 Name 从 Container 中解析请求的类型,它的用法如下:

<TextBlock
  Text="{Binding
    Path=Foo,
    Converter={prism:ContainerProvider {x:Type local:MyConverter}}}" />

<Window>
  <Window.DataContext>
    <prism:ContainerProvider Type="{x:Type local:MyViewModel}" />
  </Window.DataContext>
</Window>

5. ViewModelLocator

Prism 还提供了 ViewModelLocator,用于将 View 的 DataContext 设置为对应的 ViewModel:

<Window x:Class="Demo.Views.MainWindow"
    ...
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True">

在将 View 的 ViewModelLocator.AutoWireViewModel 附加属性设置为 True 的同时,Prism 会为查找这个 View 对应的 ViewModel 类型,然后从 Container 中解析这个类型并设置为 View 的 DataContext。它首先查找 ViewModelLocationProvider 中已经使用 Register 注册的类型,Register 函数的使用方式如下:

ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();

如果类型未在 ViewModelLocationProvider 中注册,则根据约定好的命名方式找到 ViewModel 的类型,这是默认的查找逻辑的源码:

var viewName = viewType.FullName;
viewName = viewName.Replace(".Views.", ".ViewModels.");
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
var viewModelName = String.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", viewName, suffix, viewAssemblyName);
return Type.GetType(viewModelName);

例如 PrismTest.Views.MainView 这个类,对应的 ViewModel 类型就是 PrismTest.ViewModels.MainViewModel

当然很多项目都不符合这个命名规则,那么可以在 App.xaml.cs 中重写 ConfigureViewModelLocator 并调用 ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver 改变这个查找规则:

protected override void ConfigureViewModelLocator()
{
    base.ConfigureViewModelLocator();

    ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
    {
        var viewName = viewType.FullName.Replace(".ViewModels.", ".CustomNamespace.");
        var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
        var viewModelName = $"{viewName}ViewModel, {viewAssemblyName}";
        return Type.GetType(viewModelName);
    });
}

6. Dialog Service

Prism 7 和 8 相对于以往的版本最大的改变在于 View 和 ViewModel 的交互,现在的处理方式变得更加易于使用,这篇文章以其中的 DialogService 作为代表讲解 Prism 如何实现 View 和 ViewModel 之间的交互。

DialogService 内部会调用 ViewModelLocator.AutoWireViewModel,所以使用 DialogService 调用的 View 无需添加这个附加属性。

以往在 WPF 中需要弹出一个窗口,首先新建一个 Window,然后调用 ShowDialogShowDialog 阻塞当前线程,直到弹出的 Window 关闭,这时候还可以拿到一个返回值,具体代码差不多是这样:

var window = new CreateUserWindow { Owner = this };
var dialogResult = window.ShowDialog();
if (dialogResult == true)
{
    var user = window.User;
    //other code;
}

简单直接有用。但在 MVVM 模式中,开发者要假装自己不知道要调用的 View,甚至不知道要调用的 ViewModel。开发者只知道要执行的这个操作的名字,要传什么参数,拿到什么结果,至于具体由谁去执行,开发者要假装不知道(虽然很可能都是自己写的)。为了做到这种效果,Prism 提供了 IDialogService 接口。这个接口的具体实现已经在 PrismApplication 里注册了,用户通常只需要从构造函数里注入这个服务:

public MainWindowViewModel(IDialogService dialogService)
{
    _dialogService = dialogService;
}

IDialogService 提供两组函数,分别是 Show 和 ShowDialog,对应非模态和模态窗口。它们的参数都一样:弹出的对话框的名称、传入的参数、对话框关闭时调用的回调函数:

void ShowDialog(string name, IDialogParameters parameters, Action<IDialogResult> callback);

其中 IDialogResult 类型包含 ButtonResult 类型的 Result 属性和 IDialogParameters 类型的 Parameters 属性,前者用于标识关闭对话框的动作(Yes、No、Cancel等),后者可以传入任何类型的参数作为具体的返回结果。下面代码展示了一个基本的 ShowDialog 函数调用方式:

var parameters = new DialogParameters
{
    { "UserName", "Admin" }
};

_dialogService.ShowDialog("CreateUser", parameters, dialogResult =>
{
    if (dialogResult.Result == ButtonResult.OK)
    {
        var user = dialogResult.Parameters.GetValue<User>("User");
        //other code
    }
});

为了让 IDialogService 知道上面代码中 “CreateUser” 对应的 View,需要在 'App,xaml.cs' 中的 RegisterTypes 函数中注册它对应的 Dialog:

containerRegistry.RegisterDialog<CreateUserView>("CreateUser");

上面这种注册方式需要依赖 ViewModelLocator 找到对应的 ViewModel,也可以直接注册 View 和对应的 ViewModel:

containerRegistry.RegisterDialog<CreateUserView, CreateUserViewModel>("CreateUser");

有没有发现上面的 CreateUserWindow 变成了 CreateUserView?因为使用 DialogService 的时候,View 必须是一个 UserControl,DialogService 自己创建一个 Window 将 View 放进去。这样做的好处是 View 可以不清楚自己是一个弹框或者导航的页面,或者要用在拥有不同 Window 样式的其它项目中,反正只要实现逻辑就好了。由于 View 是一个 UserControl,它不能直接控制拥有它的 Window,只能通过在 View 中添加附加属性定义 Window 的样式:

<prism:Dialog.WindowStyle>
    <Style TargetType="Window">
        <Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterScreen" />
        <Setter Property="ResizeMode" Value="NoResize"/>
        <Setter Property="ShowInTaskbar" Value="False"/>
        <Setter Property="SizeToContent" Value="WidthAndHeight"/>
    </Style>
</prism:Dialog.WindowStyle>

最后一步是实现 ViewModel。对话框的 ViewModel 必须实现 IDialogAware 接口,它的定义如下:

public interface IDialogAware
{
    /// <summary>
    /// 确定是否可以关闭对话框。
    /// </summary>
    bool CanCloseDialog();

    /// <summary>
    /// 关闭对话框时调用。
    /// </summary>
    void OnDialogClosed();

    /// <summary>
    /// 在对话框打开时调用。
    /// </summary>
    void OnDialogOpened(IDialogParameters parameters);

    /// <summary>
    /// 将显示在窗口标题栏中的对话框的标题。
    /// </summary>
    string Title { get; }

    /// <summary>
    /// 指示 IDialogWindow 关闭对话框。
    /// </summary>
    event Action<IDialogResult> RequestClose;
}

一个简单的实现如下:

public class CreateUserViewModel : BindableBase, IDialogAware
{
    public string Title => "Create User";

    public event Action<IDialogResult> RequestClose;

    private DelegateCommand _createCommand;
    public DelegateCommand CreateCommand => _createCommand ??= new DelegateCommand(Create);

    private string _userName;
    public string UserName
    {
        get { return _userName; }
        set { SetProperty(ref _userName, value); }
    }

    public virtual void RaiseRequestClose(IDialogResult dialogResult)
    {
        RequestClose?.Invoke(dialogResult);
    }

    public virtual bool CanCloseDialog()
    {
        return true;
    }

    public virtual void OnDialogClosed()
    {

    }

    public virtual void OnDialogOpened(IDialogParameters parameters)
    {
        UserName = parameters.GetValue<string>("UserName");
    }

    protected virtual void Create()
    {
        var parameters = new DialogParameters
        {
            { "User", new User{Name=UserName} }
        };

        RaiseRequestClose(new DialogResult(ButtonResult.OK, parameters));
    }
}

上面的代码在 OnDialogOpened 中读取传入的参数,在 RaiseRequestClose 关闭对话框并传递结果。至此就完成了弹出对话框并获取结果的整个流程。

自定义 Window 样式在 WPF 程序中很流行,DialogService 也支持自定义 Window 样式。假设 MyWindow 是一个自定义样式的 Window,自定义一个继承它的 MyPrismWindow 类型,并实现接口 IDialogWindow

public partial class MyPrismWindow: MyWindow, IDialogWindow
{
    public IDialogResult Result { get; set; }
}

然后调用 RegisterDialogWindow 注册这个 Window 类型。

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterDialogWindow<MyPrismWindow>();
}

这样 DialogService 将会使用这个自定义的 Window 类型作为 View 的窗口。

7. 结语

这篇文章介绍了如何使用 Prism.Wpf 创建一个 WPF 程序。虽然只介绍了 IDialogService,但其它模块也大同小异,为了让这篇文章尽量简短我舍弃了它们的说明。

如果讨厌 Prism.Wpf 的臃肿,或者需要创建面向多个 UI 平台的项目,也可以只使用轻量的 Prism.Core。

如果已经厌倦了 Prism,可以试试即将发布的 MVVM Toolkit,它基本就是个 MVVM Light 的性能加强版,而且也更时髦。

WPF MVVM从入门到精通1:MVVM模式简介_wpf 第一个mvvm_还是叫明的博客-CSDN博客

刚开始接触和使用MVVM模式的时候,就有一种感觉:哇,实现这么一丁点的功能,竟然要写这么多代码,太麻烦了吧!但是后来当我熟悉了这种模式之后,感觉就变成了:哇,还是这么麻烦。

没错,使用MVVM模式的确要在项目中增加很多代码。不过MVVM设计模式是有它的优点的,不然就不会存在。把界面和业务逻辑分离,这是MVVM的根本目的。WPF的依赖属性、数据绑定等机制,很好地帮助我们实现MVVM模式,基本可以做到在界面层不出现业务逻辑代码。

我们先来看一下MVVM模式的基本结构:

View就是界面,可以理解为XAML文件创作的内容。

Model是数据,是界面需要的数据。有人会在Model这一层里加入少量的逻辑代码,但我认为,这样破坏了Model的纯净性。

最复杂的内容都放在ViewModel里面。很多时候,逻辑代码也是可以分层、分块的,可以把这些代码放在一个辅助类库里面,然后ViewModel去调用它。

一般情况下,一个View对应一个ViewModel和一个Model。但在某些场合可以适当调整。例如,界面中某些数据是需要保存到文件的,某些只是辅助界面的显示而已,这时候,我们可以使用多个Model去存放。

最理想的情况下,实现MVVM模式后,跟View关联的CS文件会是如下面的代码所示:

 
  1. using LoginDemo.ViewModel.Login;
  2. using System.Windows;
  3.  
  4. namespace LoginDemo
  5. {
  6. /// <summary>
  7. /// LoginWindow.xaml 的交互逻辑
  8. /// </summary>
  9. public partial class LoginWindow : Window
  10. {
  11. public LoginWindow()
  12. {
  13. InitializeComponent();
  14.  
  15. this.DataContext = new LoginViewModel();
  16. }
  17. }
  18. }
 

对,其实就是在自动生成的代码里面加入了this.DataContext = new LoginViewModel();这一行。除此之外,这个文件再没有其他代码了。而实际上(带有个人想法),在一般的项目中,后台代码都没必要这么简洁。有些界面操作,是非常固定的,没必须写一大堆代码来实现。例如点击一个按钮就关闭窗口,这个功能没必要关联命令、关联事件去实现了。

我们同时从上面的代码看出,View和ViewModel的交互是通过DataContext这一对象来完成的。先上图:

在View里面,存在着大量的绑定语句。例如,用户名输入框绑定了名称为UserName的属性,性别男单选框绑定了属性Gender,当它的值为1时显示为选中。当然,我们也希望动作可以反过来。也就是在用户名输入框改变文字时,UserName也相应地改变,点击性别男时,Gender被设为1。在View里面,不需要写任何后台代码,只通过绑定就能完成前面所说的功能。有趣的是,UserName和Gender这两个属性究竟在哪里定义(甚至不一定存在),View是不关心的。它一直在等待着消息,当有人告诉它,UserName已经改成XXX了,它马上把输入框的内容改成XXX。当用户在输入框里输入OOO时,它就像广播一样,大声地喊:UserName要改成OOO了。究竟有没有人听到并做出处理呢?它并不关心。

ViewModel里面定义了大量的依赖属性。当这些属性改变时,它们会触发一个属性更改的通知事件。例如UserName="XXX";这个语句执行后,ViewModel就像广播一样大声地喊:UserName改成XXX了!同样的,有没有人听到,如何处理,它都不关心。

如此我们可以看到,在开始一个项目之前,只要我们把界面草图画好,功能大致确定好,程序员和美工就可以分头行事,各干各的了。这就是MVVM模式要做到的事情。

在后面的登录界面例子中,我们将一步步实现一个比较理想的MVVM模式程序。

我们究竟要做一个怎样的东西呢?直接上图:

这看起来比较简单,但把这个登录窗口做完,MVVM的入门就基本完成了。(为什么登录界面要选择性别这么奇怪?无非是因为RadioButton的绑定也是一个课题)

很多教程都是举一个小例子,让人刚开始接触的时候不知道如何在项目中使用。我这里从一个项目的开发角度简单说说。

首先,这个窗口只是一个项目众多窗口中的其中一个。为简单起见,我们把项目文件安排如下:

我们新建了一个ViewModel文件夹,里面按View的内容分文件夹,然后每个文件夹里面包含了Model类和ViewModel类。同时,ViewModel文件夹里有一个Common文件夹,存放一些ViewModel需要共用的类。当然,如果读者有更好的想法,完全没必要按这个模式去做。

项目开始,我们不急着写代码,而是研究一下,在View里面都包含了哪些数据。我列了一个表格:

中文名 类型 英文名
用户名 string UserName
密码 string Password
性别 int Gender
窗口初始化 View->ViewModel WndInit
登录事件 View->ViewModel LoginClick
关闭行为 ViewModel->View ToClose
打开新窗口 ViewModel->View OpenWnd

前三项是显而易见的,后面四项可能我们并不认为是一种数据。但在MVVM模式下,狭义的数据、事件、行为都变成了可以绑定的一种元素,也可以说是数据。

我们在前面提过,View和ViewModel改变状态时,都是通过类似广播的方式去做的。它们不会传递对象,而只会传递一个名称。所以,为了可以让程序员和美工分头行事,在命名方面,我们应该一开始就固定下来。

现在,我们可以开始开发Model层了。Model层的代码如下:

 
  1. namespace LoginDemo.ViewModel.Login
  2. {
  3. /// <summary>
  4. /// 登录窗口Model
  5. /// </summary>
  6. public class LoginModel
  7. {
  8. /// <summary>
  9. /// 用户名
  10. /// </summary>
  11. public string UserName { get; set; }
  12.  
  13. /// <summary>
  14. /// 密码
  15. /// </summary>
  16. public string Password { get; set; }
  17.  
  18. /// <summary>
  19. /// 性别
  20. /// </summary>
  21. public int Gender { get; set; }
  22. }
  23. }
 

Model层的代码就是这样,非常单纯,也没有什么新的知识。虽然我们后面会实现各种交互的逻辑,但Model层的代码已经不会改变了。

我们前面已经说过,现在后端和前端可以分头行事了。我们先来看看后端要做的事情。

对应于用户名输入框,ViewModel里面应该有一个相应的对象。当这个对象状态发生改变时,需要向View发出一个通知。因为所有的属性都要做这么一个事情,我们把通知这件事放到一个基类里面。

 
  1. using System.ComponentModel;
  2.  
  3. namespace LoginDemo.ViewModel.Common
  4. {
  5. public abstract class NotificationObject : INotifyPropertyChanged
  6. {
  7. public event PropertyChangedEventHandler PropertyChanged;
  8.  
  9. /// <summary>
  10. /// 发起通知
  11. /// </summary>
  12. /// <param name="propertyName">属性名</param>
  13. public void RaisePropertyChanged(string propertyName)
  14. {
  15. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  16. }
  17. }
  18. }
 

这个基类所做的事情,就是当我们调用RaisePropertyChanged的时候,就会通知View,propertyName这个属性改变了,你要做出相应的处理了。

那我们现在的ViewModel,应该是怎样的呢,代码如下:

 
  1. using LoginDemo.ViewModel.Common;
  2.  
  3. namespace LoginDemo.ViewModel.Login
  4. {
  5. public class LoginViewModel : NotificationObject
  6. {
  7. public LoginViewModel()
  8. {
  9. obj.UserName = "test";
  10. }
  11.  
  12. /// <summary>
  13. /// Model对象
  14. /// </summary>
  15. private LoginModel obj = new LoginModel();
  16.  
  17. /// <summary>
  18. /// 用户名
  19. /// </summary>
  20. public string UserName
  21. {
  22. get
  23. {
  24. return obj.UserName;
  25. }
  26. set
  27. {
  28. obj.UserName = value;
  29. this.RaisePropertyChanged("UserName");
  30. }
  31. }
  32. }
  33. }
 

我们声明了一个LoginModel的私有对象,用于存储真正的数据内容。而属性UserName,当设置它的值时,它就会发出名为"UserName"这个属性改变的通知。

可能有的人会觉得,Model和ViewModel有太多重复的内容。或许有人会删掉Model类,然后把ViewModel改成这样:

 
  1. using LoginDemo.ViewModel.Common;
  2.  
  3. namespace LoginDemo.ViewModel.Login
  4. {
  5. public class LoginViewModel : NotificationObject
  6. {
  7. private string userName;
  8. /// <summary>
  9. /// 用户名
  10. /// </summary>
  11. public string UserName
  12. {
  13. get
  14. {
  15. return userName;
  16. }
  17. set
  18. {
  19. userName = value;
  20. this.RaisePropertyChanged("UserName");
  21. }
  22. }
  23. }
  24. }
 

这样的设计在最终效果上是一样的,但我并不建议。我们可能会遇到这样的场合:把所有数据保存到一个文件,然后在下次打开软件的时候还原。如果有Model类,我们使用序列化就可以很方便地实现这一功能。当然,Model类存在的理由并不止这一个。所以虽然麻烦一点,我还是建议做一个Model类。

那么后端还要做些什么呢?其实对于UserName的处理,已经完成了。我们现在来看看前端要做的事情。

我们说过,前端存在着大量的绑定。我们使用绑定的方法,把用户名输入框绑定到名为UserName的属性上。

<TextBox Grid.Row="0" Grid.Column="1" Margin="5" Text="{Binding UserName}"/>

代码是相当简单的。我们现在就可以运行软件,然后看到用户名输入框里显示test。当我们修改其内容,在输入框失去焦点时,ViewModel里面的UserName也会变成输入框输入的内容。

至此,前后端的工作都完成了。

这一部分我们要做的事情,是把点击登录按钮的事件也在ViewModel里实现。若不是用MVVM模式,可能XAML文件里是这样的:

<Button Grid.Row="3" Grid.ColumnSpan="2" Content="登录" Width="200" Height="30" Click="Button_Click"/>

而跟XAML文件相关的CS文件里则是这样的:

 
  1. private void Button_Click(object sender, RoutedEventArgs e)
  2. {
  3. //业务处理逻辑代码
  4. }
 

如此一来,前端和后端的代码又耦合在一起了。其实,命令和事件都是可以绑定的,就像数据一样。

我们先来了解一下命令。ICommand是所有命令的接口,它主要完成两件事情,这个命令能否被执行,以及执行命令。

 
  1. event EventHandler CanExecuteChanged;
  2. public bool CanExecute(object parameter);
  3. public void Execute(object parameter);
 

例如当用户名为空时,我们可能会禁用按钮。当登录按钮跟一个命令绑定在一起时,CanExecute会不断被执行,如果返回false,按钮的IsEnabled属性也会被置为false。

一般情况下,我们需要继承ICommand接口来进行开发。

 
  1. using System;
  2. using System.Windows.Input;
  3.  
  4. namespace LoginDemo.ViewModel.Common
  5. {
  6. /// <summary>
  7. /// 命令基类
  8. /// </summary>
  9. public class BaseCommand : ICommand
  10. {
  11. public event EventHandler CanExecuteChanged
  12. {
  13. add
  14. {
  15. if (_canExecute != null)
  16. {
  17. CommandManager.RequerySuggested += value;
  18. }
  19. }
  20. remove
  21. {
  22. if (_canExecute != null)
  23. {
  24. CommandManager.RequerySuggested -= value;
  25. }
  26. }
  27. }
  28.  
  29. public bool CanExecute(object parameter)
  30. {
  31. if (_canExecute == null)
  32. {
  33. return true;
  34. }
  35. return _canExecute(parameter);
  36. }
  37.  
  38. public void Execute(object parameter)
  39. {
  40. if (_execute != null && CanExecute(parameter))
  41. {
  42. _execute(parameter);
  43. }
  44. }
  45.  
  46. private Func<object, bool> _canExecute;
  47. private Action<object> _execute;
  48.  
  49. public BaseCommand(Action<object> execute, Func<object, bool> canExecute)
  50. {
  51. _execute = execute;
  52. _canExecute = canExecute;
  53. }
  54.  
  55. public BaseCommand(Action<object> execute) :
  56. this(execute, null)
  57. {
  58. }
  59. }
  60. }
 

BaseCommand的功能很简单,就是执行命令前先判断一下命令能不能执行。

然后我们就可以绑定命令了,在后端这样写:

 
  1. private BaseCommand clickLoginCommand;
  2. public BaseCommand ClickLoginCommand
  3. {
  4. get
  5. {
  6. if(clickLoginCommand==null)
  7. {
  8. clickLoginCommand = new BaseCommand(new Action<object>(o =>
  9. {
  10. //执行登录逻辑
  11. }));
  12. }
  13. return clickLoginCommand;
  14. }
  15. }
 

前端这样写:

<Button Grid.Row="3" Grid.ColumnSpan="2" Content="登录" Width="200" Height="30" Command="{Binding ClickLoginCommand}"/>

点击按钮执行登录逻辑的代码就这样完成了。但不要急着复制代码,因为我们不打算使用命令。

我们知道,对于按钮的操作,不一定是点击,可能是鼠标划过,可能是鼠标右击。那Command触发的是什么呢?就是点击,没有其他了。对于其他控件,例如是输入框,Command又代表什么呢?文本改变事件能用Command吗?这些问题让我们感到困惑,所以一般在项目中,我都只会使用事件,而不会使用命令(即使是单击事件)。

BaseCommand这个类还可以留着,我们后面还需要使用的。在引入事件之前,我们需要先引用一个dll:System.Windows.Interactivity.dll。这个dll并不是.NET Framework的标配,它是Blend的一个类库。可以在扩展的程序集里找到:

如果没有找到(我安装VS2017后就没有找到),需要安装以下库才有:

好了,引用了System.Windows.Interactivity.dll后,我们就可以开始讲事件了。

有些事件是有参数的,例如鼠标移动这个事件,会带上鼠标的位置。但我们之前使用的命令,默认传入的参数是null。为了能够传递参数,我们需要先定义一个事件基类:

 
  1. using System.Windows;
  2. using System.Windows.Input;
  3. using System.Windows.Interactivity;
  4.  
  5. namespace LoginDemo.ViewModel.Common
  6. {
  7. /// <summary>
  8. /// 事件命令
  9. /// </summary>
  10. public class EventCommand : TriggerAction<DependencyObject>
  11. {
  12. protected override void Invoke(object parameter)
  13. {
  14. if (CommandParameter != null)
  15. {
  16. parameter = CommandParameter;
  17. }
  18. if (Command != null)
  19. {
  20. Command.Execute(parameter);
  21. }
  22. }
  23.  
  24. /// <summary>
  25. /// 事件
  26. /// </summary>
  27. public ICommand Command
  28. {
  29. get { return (ICommand)GetValue(CommandProperty); }
  30. set { SetValue(CommandProperty, value); }
  31. }
  32. public static readonly DependencyProperty CommandProperty =
  33. DependencyProperty.Register("Command", typeof(ICommand), typeof(EventCommand), new PropertyMetadata(null));
  34.  
  35. /// <summary>
  36. /// 事件参数,如果为空,将自动传入事件的真实参数
  37. /// </summary>
  38. public object CommandParameter
  39. {
  40. get { return (object)GetValue(CommandParameterProperty); }
  41. set { SetValue(CommandParameterProperty, value); }
  42. }
  43. public static readonly DependencyProperty CommandParameterProperty =
  44. DependencyProperty.Register("CommandParameter", typeof(object), typeof(EventCommand), new PropertyMetadata(null));
  45. }
  46. }
 

现在,我们可以在ViewModel里增加如下代码:

 
  1. private BaseCommand loginClick;
  2. /// <summary>
  3. /// 登录事件
  4. /// </summary>
  5. public BaseCommand LoginClick
  6. {
  7. get
  8. {
  9. if(loginClick==null)
  10. {
  11. loginClick = new BaseCommand(new Action<object>(o =>
  12. {
  13. //执行登录逻辑
  14. }));
  15. }
  16. return loginClick;
  17. }
  18. }
 

然后在XAML文件里,先加入i这个命名空间:xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity",然后修改按钮的代码:

 
  1. <Button Grid.Row="3" Grid.ColumnSpan="2" Content="登录" Width="200" Height="30">
  2. <i:Interaction.Triggers>
  3. <i:EventTrigger EventName="Click">
  4. <c:EventCommand Command="{Binding LoginClick}"/>
  5. </i:EventTrigger>
  6. </i:Interaction.Triggers>
  7. </Button>
 

上面的代码指出,Click这个事件,绑定到了LoginClick这个属性。当我们点击按钮的时候,LoginClick里面的Action就会被执行。

读到这里,可能有的读者会发现,我们只讲了用户名的绑定,然后就说到登录事件了,跳过了密码框和单选框的绑定。是因为这两者完全类似,不需要讲吗?并不是,而是因为它们涉及到了新的课题。

对于PasswordBox,可能很多人都会按着TextBox的路子,在ViewModel里面写一个属性,然后绑定到Password属性上。当你写完这一切的时候,你会突然收到Visual Studio的提示:Password并不是依赖属性,不能绑定!

当然,微软的工程师们这样设计是有原因的,毕竟绑定可能引起密码的泄漏问题。

那我们要怎么办呢?路子有两条:

(1)改造TextBox,把显示的字符改成***这种。

(2)改造PasswordBox,增加一个依赖属性,借助它读和写Password属性。

可能第二条路子会简单一些,我们选择这条路子。先上代码:

 
  1. using System.Windows;
  2. using System.Windows.Controls;
  3. using System.Windows.Interactivity;
  4.  
  5. namespace LoginDemo.ViewModel.Common
  6. {
  7. /// <summary>
  8. /// 增加Password扩展属性
  9. /// </summary>
  10. public static class PasswordBoxHelper
  11. {
  12. public static string GetPassword(DependencyObject obj)
  13. {
  14. return (string)obj.GetValue(PasswordProperty);
  15. }
  16.  
  17. public static void SetPassword(DependencyObject obj, string value)
  18. {
  19. obj.SetValue(PasswordProperty, value);
  20. }
  21.  
  22. public static readonly DependencyProperty PasswordProperty =
  23. DependencyProperty.RegisterAttached("Password", typeof(string), typeof(PasswordBoxHelper), new PropertyMetadata("", OnPasswordPropertyChanged));
  24.  
  25. private static void OnPasswordPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
  26. {
  27. PasswordBox box = sender as PasswordBox;
  28. string password = (string)e.NewValue;
  29. if (box != null && box.Password != password)
  30. {
  31. box.Password = password;
  32. }
  33. }
  34. }
  35.  
  36. /// <summary>
  37. /// 接收PasswordBox的密码修改事件
  38. /// </summary>
  39. public class PasswordBoxBehavior : Behavior<PasswordBox>
  40. {
  41. protected override void OnAttached()
  42. {
  43. base.OnAttached();
  44.  
  45. AssociatedObject.PasswordChanged += OnPasswordChanged;
  46. }
  47.  
  48. protected override void OnDetaching()
  49. {
  50. base.OnDetaching();
  51.  
  52. AssociatedObject.PasswordChanged -= OnPasswordChanged;
  53. }
  54.  
  55. private static void OnPasswordChanged(object sender, RoutedEventArgs e)
  56. {
  57. PasswordBox box = sender as PasswordBox;
  58. string password = PasswordBoxHelper.GetPassword(box);
  59. if (box != null && box.Password != password)
  60. {
  61. PasswordBoxHelper.SetPassword(box, box.Password);
  62. }
  63. }
  64. }
  65. }
 

我们为PasswordBox增加一个扩展属性,修改这个属性,就会修改密码。但反过来,当密码框的内容改变时,附加属性是收不到消息的。这时我们需要PasswordBoxBehavior这个行为。密码框内容改变,就会通知它,然后附加属性相应地改变自己的值。

我们把XAML文件密码框的代码改成如下所示:

 
  1. <PasswordBox Grid.Row="1" Grid.Column="1" Margin="5" c:PasswordBoxHelper.Password="{Binding Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
  2. <i:Interaction.Behaviors>
  3. <c:PasswordBoxBehavior/>
  4. </i:Interaction.Behaviors>
  5. </PasswordBox>
 

在ViewModel文件里面,增加对密码的定义:

 
  1. /// <summary>
  2. /// 密码
  3. /// </summary>
  4. public string Password
  5. {
  6. get
  7. {
  8. return obj.Password;
  9. }
  10. set
  11. {
  12. obj.Password = value;
  13. this.RaisePropertyChanged("Password");
  14. }
  15. }
 

至此,密码框的绑定就完成了。

当我们要进行性别这一属性绑定的时候,我们会发现,两个RadioButton控件都跟一个Gender属性关联。其实我们在绑定控件时还会遇到这样一个问题:属性是一个bool类型,但需要绑定控件的Visibility属性。这个问题也可以在本文介绍的方法得到解决。

控件在绑定时,是可以设置一个Converter的。Converter有两个方法,分别是Convert和ConvertBack。Convert用于将数据格式化之后,显示到控件上。而ConvertBack就是在界面端修改了控件状态,数据应该如何变化。

以下是一个比较通用的RadioButton的Converter:

 
  1. using System;
  2. using System.Globalization;
  3. using System.Windows.Data;
  4.  
  5. namespace LoginDemo.ViewModel.Common
  6. {
  7. public class CheckConverter : IValueConverter
  8. {
  9. public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  10. {
  11. if (value == null || parameter == null)
  12. {
  13. return false;
  14. }
  15. string checkvalue = value.ToString();
  16. string targetvalue = parameter.ToString();
  17. bool r = checkvalue.Equals(targetvalue);
  18. return r;
  19. }
  20.  
  21. public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  22. {
  23. if (value == null || parameter == null)
  24. {
  25. return null;
  26. }
  27.  
  28. if ((bool)value)
  29. {
  30. return parameter.ToString();
  31. }
  32. return null;
  33. }
  34. }
  35. }
 

在XAML处,需要先增加Converter的资源:

 
  1. <Window.Resources>
  2. <c:CheckConverter x:Key="CheckConverter"/>
  3. </Window.Resources>
 

然后RadioButton的代码修改如下:

 
  1. <RadioButton Grid.Row="2" Grid.Column="0" Content="男" IsChecked="{Binding Gender,Mode=TwoWay,Converter={StaticResource CheckConverter},ConverterParameter=1}"/>
  2. <RadioButton Grid.Row="2" Grid.Column="1" Content="女" IsChecked="{Binding Gender,Mode=TwoWay,Converter={StaticResource CheckConverter},ConverterParameter=2}"/>
 

ViewModel里增加的代码就没什么新意了:

 
  1. /// <summary>
  2. /// 性别
  3. /// </summary>
  4. public int Gender
  5. {
  6. get
  7. {
  8. return obj.Gender;
  9. }
  10. set
  11. {
  12. obj.Gender = value;
  13. this.RaisePropertyChanged("Gender");
  14. }
  15. }
 

这样绑定以后,当Gender=2时,性别女的单选框会被选中;当性别男的单选框被选中后,Gender会变成1。

若是登录成功,我们一般会执行的操作是关闭当前窗口,然后打开一个新的窗口。但为了比较理想地实现MVVM,我们被禁止在ViewModel里面访问View的元素。那我们该如何实现上面的功能呢?

首先是打开窗口的功能,我们使用的方法是:

(1)窗口初始化的时候即注册需要访问的新窗口。

(2)ViewModel在需要打开新窗口时,使用注册过的窗口。

我们先定义一个WindowManager类:

 
  1. using System;
  2. using System.Collections;
  3. using System.Windows;
  4.  
  5. namespace LoginDemo.ViewModel.Common
  6. {
  7. /// <summary>
  8. /// 窗口管理器
  9. /// </summary>
  10. public static class WindowManager
  11. {
  12. private static Hashtable _RegisterWindow = new Hashtable();
  13.  
  14. public static void Register<T>(string key)
  15. {
  16. if (!_RegisterWindow.Contains(key))
  17. {
  18. _RegisterWindow.Add(key, typeof(T));
  19. }
  20. }
  21.  
  22. public static void Register(string key, Type t)
  23. {
  24. if (!_RegisterWindow.Contains(key))
  25. {
  26. _RegisterWindow.Add(key, t);
  27. }
  28. }
  29.  
  30. public static void Remove(string key)
  31. {
  32. if (_RegisterWindow.ContainsKey(key))
  33. {
  34. _RegisterWindow.Remove(key);
  35. }
  36. }
  37.  
  38. public static void Show(string key, object VM)
  39. {
  40. if (_RegisterWindow.ContainsKey(key))
  41. {
  42. var win = (Window)Activator.CreateInstance((Type)_RegisterWindow[key]);
  43. win.DataContext = VM;
  44. win.Show();
  45. }
  46. }
  47. }
  48. }
 

代码比较简单,就不解释了。然后我们在LoginWindow的构造函数里添加代码,变成如下所示:

 
  1. using LoginDemo.ViewModel.Common;
  2. using LoginDemo.ViewModel.Login;
  3. using System.Windows;
  4.  
  5. namespace LoginDemo
  6. {
  7. /// <summary>
  8. /// LoginWindow.xaml 的交互逻辑
  9. /// </summary>
  10. public partial class LoginWindow : Window
  11. {
  12. public LoginWindow()
  13. {
  14. InitializeComponent();
  15.  
  16. this.DataContext = new LoginViewModel();
  17.  
  18. WindowManager.Register<MainWindow>("MainWindow");
  19. }
  20. }
  21. }
 

是不是发现这里说好只加一行,现在又加一行代码了?实在没有办法,打开窗口就是要这么做。

然后我们在ViewModel需要打开窗口的地方写下面一行代码:

WindowManager.Show("MainWindow", null);

这样新的窗口就能在ViewModel里面被打开了。

 

我们接下来说关闭窗口。要做到这一功能,我们又要借助System.Windows.Interacivity里面的Behavior。它可以把ViewModel里面的一个属性,关联到View层的一个事件(我们这里当然是要关联Window.Close())。

我们先来定义这个关闭行为:

 
  1. using System.Windows;
  2. using System.Windows.Interactivity;
  3.  
  4. namespace LoginDemo.ViewModel.Common
  5. {
  6. /// <summary>
  7. /// 窗口行为
  8. /// </summary>
  9. public class WindowBehavior : Behavior<Window>
  10. {
  11. /// <summary>
  12. /// 关闭窗口
  13. /// </summary>
  14. public bool Close
  15. {
  16. get { return (bool)GetValue(CloseProperty); }
  17. set { SetValue(CloseProperty, value); }
  18. }
  19. public static readonly DependencyProperty CloseProperty =
  20. DependencyProperty.Register("Close", typeof(bool), typeof(WindowBehavior), new PropertyMetadata(false, OnCloseChanged));
  21. private static void OnCloseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  22. {
  23. var window = ((WindowBehavior)d).AssociatedObject;
  24. var newValue = (bool)e.NewValue;
  25. if (newValue)
  26. {
  27. window.Close();
  28. }
  29. }
  30.  
  31. }
  32. }
 

然后我们在XAML文件里增加以下内容:

 
  1. <i:Interaction.Behaviors>
  2. <c:WindowBehavior Close="{Binding ToClose}"/>
  3. </i:Interaction.Behaviors>
 

这样的话,窗口的关闭事件就绑定到了ViewModel里面的ToClose属性了。但这个属性还没有呢,定义一个:

 
  1. private bool toClose = false;
  2. /// <summary>
  3. /// 是否要关闭窗口
  4. /// </summary>
  5. public bool ToClose
  6. {
  7. get
  8. {
  9. return toClose;
  10. }
  11. set
  12. {
  13. toClose = value;
  14. if (toClose)
  15. {
  16. this.RaisePropertyChanged("ToClose");
  17. }
  18. }
  19. }
 

如此,只要我们在ViewModel里面执行ToClose=true;,当前窗口就会关闭。这节的内容体现在点击登录按钮上,大体如下:

 
  1. private BaseCommand loginClick;
  2. /// <summary>
  3. /// 登录事件
  4. /// </summary>
  5. public BaseCommand LoginClick
  6. {
  7. get
  8. {
  9. if (loginClick == null)
  10. {
  11. loginClick = new BaseCommand(new Action<object>(o =>
  12. {
  13. //执行登录逻辑
  14. WindowManager.Show("MainWindow", null);
  15. ToClose = true;
  16. }));
  17. }
  18. return loginClick;
  19. }
  20. }
 

到目前为止,登录窗口的基本功能似乎都完成了。但我们知道,很多时候用户名的格式是有要求的,例如是只有字母数字下划线,或者字数有限制。这要求我们在登录之前,验证输入内容的正确性。在这一节,我们需要验证用户名和密码的正确性,如果上面两个框的输入非法,禁用登录按钮。

在数据验证错误的时候,我们显示一个叹号在输入框的旁边,如下图所示:

数据验证的方法有很多,我们使用了一种比较优雅的。

首先定义一些验证属性:

 
  1. using System.ComponentModel.DataAnnotations;
  2.  
  3. namespace LoginDemo.ViewModel.Login
  4. {
  5. public class NotEmptyCheck : ValidationAttribute
  6. {
  7. public override bool IsValid(object value)
  8. {
  9. var name = value as string;
  10. if (string.IsNullOrEmpty(name))
  11. {
  12. return false;
  13. }
  14. return true;
  15. }
  16.  
  17. public override string FormatErrorMessage(string name)
  18. {
  19. return "不能为空";
  20. }
  21. }
  22.  
  23. public class UserNameExists : ValidationAttribute
  24. {
  25. public override bool IsValid(object value)
  26. {
  27. var name = value as string;
  28. if (name.Contains("abc"))
  29. {
  30. return true;
  31. }
  32. return false;
  33. }
  34.  
  35. public override string FormatErrorMessage(string name)
  36. {
  37. return "用户名必须包含abc";
  38. }
  39. }
  40. }
 

第一个验证属性要求宿主的内容不能为空,第二个验证属性要求内容必须含有abc这个字符串。

然后我们又要用到Behavior了。当绑定的内容校验出异常后,它会一起冒泡,只到Window。这时候,Window的Behavior接收到异常,做出相应的处理。

 
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Windows;
  4. using System.Windows.Controls;
  5. using System.Windows.Documents;
  6. using System.Windows.Interactivity;
  7.  
  8. namespace LoginDemo.ViewModel.Common
  9. {
  10. /// <summary>
  11. /// 验证异常行为
  12. /// </summary>
  13. public class ValidationExceptionBehavior : Behavior<FrameworkElement>
  14. {
  15. /// <summary>
  16. /// 记录异常的数量
  17. /// </summary>
  18. /// <remarks>在一个页面里面,所有控件的验证错误信息都会传到这个类上,每个控制需不需要显示验证错误,需要分别记录</remarks>
  19. private Dictionary<UIElement, int> ExceptionCount;
  20. /// <summary>
  21. /// 缓存页面的提示装饰器
  22. /// </summary>
  23. private Dictionary<UIElement, NotifyAdorner> AdornerDict;
  24.  
  25. protected override void OnAttached()
  26. {
  27. ExceptionCount = new Dictionary<UIElement, int>();
  28. AdornerDict = new Dictionary<UIElement, NotifyAdorner>();
  29.  
  30. this.AssociatedObject.AddHandler(Validation.ErrorEvent, new EventHandler<ValidationErrorEventArgs>(OnValidationError));
  31. }
  32.  
  33. /// <summary>
  34. /// 当验证错误信息改变时,首先调用此函数
  35. /// </summary>
  36. private void OnValidationError(object sender, ValidationErrorEventArgs e)
  37. {
  38. try
  39. {
  40. var handler = GetValidationExceptionHandler();//插入<c:ValidationExceptionBehavior></c:ValidationExceptionBehavior>此语句的窗口的DataContext,也就是ViewModel
  41. var element = e.OriginalSource as UIElement;//错误信息发生改变的控件
  42. if (handler == null || element == null)
  43. {
  44. return;
  45. }
  46.  
  47. if (e.Action == ValidationErrorEventAction.Added)
  48. {
  49. if (ExceptionCount.ContainsKey(element))
  50. {
  51. ExceptionCount[element]++;
  52. }
  53. else
  54. {
  55. ExceptionCount.Add(element, 1);
  56. }
  57. }
  58. else if (e.Action == ValidationErrorEventAction.Removed)
  59. {
  60. if (ExceptionCount.ContainsKey(element))
  61. {
  62. ExceptionCount[element]--;
  63. }
  64. else
  65. {
  66. ExceptionCount.Add(element, -1);
  67. }
  68. }
  69.  
  70. if (ExceptionCount[element] <= 0)
  71. {
  72. HideAdorner(element);
  73. }
  74. else
  75. {
  76. ShowAdorner(element, e.Error.ErrorContent.ToString());
  77. }
  78.  
  79. int TotalExceptionCount = 0;
  80. foreach (KeyValuePair<UIElement, int> kvp in ExceptionCount)
  81. {
  82. TotalExceptionCount += kvp.Value;
  83. }
  84.  
  85. handler.IsValid = (TotalExceptionCount <= 0);//ViewModel里面的IsValid
  86. }
  87. catch (Exception ex)
  88. {
  89. throw ex;
  90. }
  91. }
  92.  
  93. /// <summary>
  94. /// 获得行为所在窗口的DataContext
  95. /// </summary>
  96. private NotificationObject GetValidationExceptionHandler()
  97. {
  98. if (this.AssociatedObject.DataContext is NotificationObject)
  99. {
  100. var handler = this.AssociatedObject.DataContext as NotificationObject;
  101.  
  102. return handler;
  103. }
  104.  
  105. return null;
  106. }
  107.  
  108. /// <summary>
  109. /// 显示错误信息提示
  110. /// </summary>
  111. private void ShowAdorner(UIElement element, string errorMessage)
  112. {
  113. if (AdornerDict.ContainsKey(element))
  114. {
  115. AdornerDict[element].ChangeToolTip(errorMessage);
  116. }
  117. else
  118. {
  119. var adornerLayer = AdornerLayer.GetAdornerLayer(element);
  120. NotifyAdorner adorner = new NotifyAdorner(element, errorMessage);
  121. adornerLayer.Add(adorner);
  122. AdornerDict.Add(element, adorner);
  123. }
  124. }
  125.  
  126. /// <summary>
  127. /// 隐藏错误信息提示
  128. /// </summary>
  129. private void HideAdorner(UIElement element)
  130. {
  131. if (AdornerDict.ContainsKey(element))
  132. {
  133. var adornerLayer = AdornerLayer.GetAdornerLayer(element);
  134. adornerLayer.Remove(AdornerDict[element]);
  135. AdornerDict.Remove(element);
  136. }
  137. }
  138. }
  139. }
 

这里异常的处理方式是显示我们最开始戴图的叹号图形。这个图形由NotifyAdnoner完成显示:

 
  1. using System;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Documents;
  5. using System.Windows.Media;
  6. using System.Windows.Media.Imaging;
  7.  
  8. namespace LoginDemo.ViewModel.Common
  9. {
  10. /// <summary>
  11. /// 带有感叹号的提示图形
  12. /// </summary>
  13. public class NotifyAdorner : Adorner
  14. {
  15. private VisualCollection _visuals;
  16. private Canvas _canvas;
  17. private Image _image;
  18. private TextBlock _toolTip;
  19.  
  20. public NotifyAdorner(UIElement adornedElement, string errorMessage) : base(adornedElement)
  21. {
  22. _visuals = new VisualCollection(this);
  23.  
  24. _image = new Image()
  25. {
  26. Width = 16,
  27. Height = 16,
  28. Source = new BitmapImage(new Uri("/warning.png", UriKind.RelativeOrAbsolute))
  29. };
  30.  
  31. _toolTip = new TextBlock() { Text = errorMessage };
  32. _image.ToolTip = _toolTip;
  33.  
  34. _canvas = new Canvas();
  35. _canvas.Children.Add(_image);
  36. _visuals.Add(_canvas);
  37. }
  38.  
  39. protected override int VisualChildrenCount
  40. {
  41. get
  42. {
  43. return _visuals.Count;
  44. }
  45. }
  46.  
  47. protected override Visual GetVisualChild(int index)
  48. {
  49. return _visuals[index];
  50. }
  51.  
  52. public void ChangeToolTip(string errorMessage)
  53. {
  54. _toolTip.Text = errorMessage;
  55. }
  56.  
  57. protected override Size MeasureOverride(Size constraint)
  58. {
  59. return base.MeasureOverride(constraint);
  60. }
  61.  
  62. protected override Size ArrangeOverride(Size finalSize)
  63. {
  64. _canvas.Arrange(new Rect(finalSize));
  65. _image.Margin = new Thickness(finalSize.Width + 3, 0, 0, 0);
  66.  
  67. return base.ArrangeOverride(finalSize);
  68. }
  69. }
  70. }
 

我们的ViewModel也要对数据验证做出支持。由于我们先前让ViewModel继承了NotificationObject,它并不是一个接口,我们不能继承两个类。所以,我们在NotificationObject里面加入验证有内容(虽然这样不太好)。

 
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.ComponentModel.DataAnnotations;
  5. using System.Linq;
  6.  
  7. namespace LoginDemo.ViewModel.Common
  8. {
  9. public abstract class NotificationObject : INotifyPropertyChanged, IDataErrorInfo
  10. {
  11. #region 属性修改通知
  12.  
  13. public event PropertyChangedEventHandler PropertyChanged;
  14.  
  15. /// <summary>
  16. /// 发起通知
  17. /// </summary>
  18. /// <param name="propertyName">属性名</param>
  19. public void RaisePropertyChanged(string propertyName)
  20. {
  21. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  22. }
  23.  
  24. #endregion
  25.  
  26. #region 数据验证
  27.  
  28. public string Error
  29. {
  30. get { return ""; }
  31. }
  32.  
  33. public string this[string columnName]
  34. {
  35. get
  36. {
  37. var vc = new ValidationContext(this, null, null);
  38. vc.MemberName = columnName;
  39. var res = new List<ValidationResult>();
  40. var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res);
  41. if (res.Count > 0)
  42. {
  43. return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray());
  44. }
  45. return string.Empty;
  46. }
  47. }
  48.  
  49. /// <summary>
  50. /// 页面中是否所有控制数据验证正确
  51. /// </summary>
  52. public virtual bool IsValid { get; set; }
  53.  
  54. #endregion
  55. }
  56. }
 

至此,准备就绪。我们修改ViewModel里面的UserName和Password属性:

 
  1. /// <summary>
  2. /// 用户名
  3. /// </summary>
  4. [NotEmptyCheck]
  5. [UserNameExists]
  6. public string UserName
  7. {
  8. get
  9. {
  10. return obj.UserName;
  11. }
  12. set
  13. {
  14. obj.UserName = value;
  15. this.RaisePropertyChanged("UserName");
  16. }
  17. }
  18.  
  19. /// <summary>
  20. /// 密码
  21. /// </summary>
  22. [NotEmptyCheck]
  23. public string Password
  24. {
  25. get
  26. {
  27. return obj.Password;
  28. }
  29. set
  30. {
  31. obj.Password = value;
  32. this.RaisePropertyChanged("Password");
  33. }
  34. }
 

没错,就是加了头上中括号的内容。这样的话,UserName就被要求非空和包含abc,而密码则被要求非空。由于我们在NotificationObject里加入了IsValid虚属性,还必须实现一下:

 
  1. /// <summary>
  2. /// 数据填写正确
  3. /// </summary>
  4. public override bool IsValid
  5. {
  6. get
  7. {
  8. return obj.IsValid;
  9. }
  10. set
  11. {
  12. if (value == obj.IsValid)
  13. {
  14. return;
  15. }
  16. obj.IsValid = value;
  17. this.RaisePropertyChanged("IsValid");
  18. }
  19. }
 

这个IsValid的设置是在ValidationExceptionBehavior里完成的。登录按钮只要绑定这个属性,就能在出现验证异常时,变成禁用。

我们修改XAML文件的用户名、密码和登录按钮:

 
  1. <TextBox Grid.Row="0" Grid.Column="1" Margin="5" Text="{Binding UserName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"/>
  2.  
  3. <PasswordBox Grid.Row="1" Grid.Column="1" Margin="5" c:PasswordBoxHelper.Password="{Binding Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True,ValidatesOnDataErrors=True,NotifyOnValidationError=True}">
  4. <i:Interaction.Behaviors>
  5. <c:PasswordBoxBehavior/>
  6. </i:Interaction.Behaviors>
  7. </PasswordBox>
  8.  
  9. <Button Grid.Row="3" Grid.ColumnSpan="2" Content="登录" Width="200" Height="30" IsEnabled="{Binding IsValid}">
  10. <i:Interaction.Triggers>
  11. <i:EventTrigger EventName="Click">
  12. <c:EventCommand Command="{Binding LoginClick}"/>
  13. </i:EventTrigger>
  14. </i:Interaction.Triggers>
  15. </Button>
 

窗口刚打开的时候是这样的,登录按钮被禁用:

当数据都输入正确,登录按钮被启用:

至此,登录窗口的所有功能就介绍完了。也恭喜你,你已经能熟练地使用MVVM模式了。

AOP 面向切面编程--WPF Prism.Unity框架中集成的AOP_prism aop_如果我来了6的博客-CSDN博客

概念

1、Aspect-Oriented Programming(面向切面编程),AOP就是对OOP(面向对象编程)的一种功能扩展。
2、需要将核心业务与公共业务分离。

AOP优势

1、将通用功能从业务逻辑中抽离出来,可以省略大量重复代码,有利于代码的操作和维护。
2、模块化开发,降低软件架构的复杂度。

静态AOP

采用装饰器模式或者代理模式实现。

抽象类与实现类:

    public interface IInfoProcessor
    {
        void ReadInfo(string info);
    }
    public class InfoProcessor : IInfoProcessor
    {
        public InfoProcessor()
        {
            Console.WriteLine("InfoProcessor 构造函数执行了");
        }

        public void ReadInfo(string info)
        {
            Console.WriteLine($"主要业务逻辑,接收到一条信息或请求:{info}");
        }
    }

装饰器模式实现:

	/// <summary>
    /// 装饰器模式提供一个AOP功能
    /// </summary>
    public class InfoProcessorDecorator : IInfoProcessor
    {
        private IInfoProcessor _infoProcessor { get; set; }
        public InfoProcessorDecorator(IInfoProcessor infoProcessor)
        {
            this._infoProcessor = infoProcessor;
        }

        public void ReadInfo(string info)
        {
            this.BeforeProceed(info);
            this._infoProcessor.ReadInfo(info);
            this.AfterProceed(info);
        }

        /// <summary>
        /// 业务逻辑之前
        /// </summary>
        /// <param name="user"></param>
        private void BeforeProceed(string info)
        {
            Console.WriteLine("方法执行前");
        }
        /// <summary>
        /// 业务逻辑之后
        /// </summary>
        /// <param name="user"></param>
        private void AfterProceed(string info)
        {
            Console.WriteLine("方法执行后");
        }
    }

上端调用:

        public static void Execute()
        {
            /// 使用了设计模式:装饰模式
            /// 好像功能实现,但是,需要对多个类进行切面的时候,需要创建多个Decorator
            /// 没有通用性
            IInfoProcessor processor = new InfoProcessor();// 实际的业务逻辑
            processor = new InfoProcessorDecorator(processor);
            processor.ReadInfo("哈哈哈");
        }

动态AOP

采用Castle库实现。
AOP有很多方法可以实现,Castle的一个库,用来做动态AOP 。

抽象类与实现类:

    public interface IInfoProcessor
    {
        void ReadInfo(string info);
    }
    public class InfoProcessor : IInfoProcessor
    {
        public InfoProcessor()
        {
            Console.WriteLine("InfoProcessor 构造函数执行了");
        }
        
		/// 注意:被切面的对象方法必须是  virtual
		/// 虚方法在生成代理对象的时候可以被重写
		// 需要把切面逻辑转移到特性里面去
        [Log]
        [Monitor]
        public virtual void ReadInfo(string info)
        {
            Console.WriteLine($"主要业务逻辑,接收到一条信息或请求:{info}");
        }

        [Monitor]
        public virtual void UpdateInfo(string info)
        {

        }
    }

特性类中实现切面逻辑:

    public abstract class AbstractAttributeBase : Attribute
    {
        public abstract void Execute();
    }

    public class LogAttribute : AbstractAttributeBase
    {
        public override void Execute()
        {
            Console.WriteLine("Log 切面逻辑");
        }
    }

    public class MonitorAttribute : AbstractAttributeBase
    {
        public override void Execute()
        {
            Console.WriteLine("Monitor 切面逻辑");
        }
    }

继承接口,实现动态代理,并且将切面转移到特性中去(目前特性只实现了在业务逻辑之前加切面,如果要实现分别在业务逻辑的前后加逻辑,需要在特性中定义Before和After属性来实现;执行顺序可以增加Order属性来实现)

    public class MyInterceptor :StandardInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            if (invocation.MethodInvocationTarget.IsDefined(typeof(AbstractAttributeBase), true))
            {
                foreach (var attr in invocation.MethodInvocationTarget.GetCustomAttributes(typeof(AbstractAttributeBase), true))
                {
                    var log = attr as AbstractAttributeBase;
                    log.Execute();
                }
            }
            //this.PreProceed(invocation);
            invocation.Proceed();// 执行真正的逻辑
            //this.PostProceed(invocation);
            
            // 需要把切面逻辑转移到特性里面去
        }
        public void PreProceed(IInvocation invocation)
        {
            Console.WriteLine("方法执行前");
        }

        public void PostProceed(IInvocation invocation)
        {
            Console.WriteLine("方法执行后");
        }
    }

上端调用:

        public static void Execute()
        {
       		/// 使用代理的方式,针对不同的对象生成实例   生成一个代理对象
            ProxyGenerator generator = new ProxyGenerator();// 代理生成器
            MyInterceptor interceptor = new MyInterceptor();
            InfoProcessor userprocessor = generator.CreateClassProxy<InfoProcessor>(interceptor);               
            userprocessor.ReadInfo("哈哈哈");
        }

动态AOP与IOC整合

手动实现IOC,并在对象创建时整合动态的AOP功能

    public class MyConstructorAttribute : Attribute
    {
    }
    
    public class MyParameterAttribute : Attribute
    {
        public MyParameterAttribute(string shortName = null)
        {
            this.ShortName = shortName;

        }
        public string ShortName { get; set; }
    }
    public interface IMyIoc
    {
        // 注册
        void Register<From, To>(string shortName = null);
        // 获取实例
        From Resolve<From>(string shortName = null);
    }
	/// <summary>
    /// 第三方Ioc容器,需要与业务无关
    /// </summary>
    public class MyIoc : IMyIoc
    {
        Dictionary<string, Type> _containerDic = new Dictionary<string, Type>();

        private string GenerateKey(string abstractName, string shortName) => $"{abstractName}_@$_{shortName}";

        public void Register<AFrom, BTo>(string shortName = null)
        {
            string key = typeof(AFrom).FullName;// 获取抽象的全名称
            key = GenerateKey(key, shortName);// 根据抽象全名称和ShortName组合一个Key【字典的索引】

            if (!_containerDic.ContainsKey(key))
            {
                this._containerDic.Add(key, typeof(BTo));// 把需要创建实例的对象类型保存下来
            }
        }

        /// <summary>
        /// 1、根据抽象类型获取实例类型
        /// 2、初始化构造参数
        /// 3、构造实例
        /// </summary>
        /// <typeparam name="AFrom"></typeparam>
        /// <param name="shortName"></param>
        /// <returns></returns>
        public AFrom Resolve<AFrom>(string shortName = null)
        {
            return (AFrom)ResolveObject(typeof(AFrom), shortName);
        }

        // 依赖N层的时候,需要递归创建对象
        private object ResolveObject(Type abstractType, string shortName = null)
        {
            string key = abstractType.FullName;
            key = GenerateKey(key, shortName);

            if (!_containerDic.ContainsKey(key)) return null;

            Type type = this._containerDic[key];

            // 检查类型的构造函数
            //var ctor = type.GetConstructors()[0];

            // 通过以下方式获取标定了特性的构造函数
            ConstructorInfo constructorInfo =
            type.GetConstructors().FirstOrDefault(c => c.IsDefined(typeof(MyConstructorAttribute), true));
            if (constructorInfo == null)
                // 选择参数最多的构造函数
                constructorInfo = type.GetConstructors().OrderByDescending(c => c.GetParameters().Length).First();


            List<object> paramList = new List<object>();
            // 检查构造函数的参数
            foreach (var param in constructorInfo.GetParameters())
            {
                string paramShortName = "";
                if (param.IsDefined(typeof(MyParameterAttribute), true))
                {
                    paramShortName = ((MyParameterAttribute)param.GetCustomAttribute(typeof(MyParameterAttribute))).ShortName;
                }
                Type paramType = param.ParameterType;//参数的抽象类型
                //string paramKey = GenerateKey(paramType.FullName, "");// 参数对应的类型的Key
                //Type paramInstanceType = this._containerDic[paramKey];// 需要实例化的类型
                //object paramInstance = Activator.CreateInstance(paramInstanceType);// 创建参数实例
                object paramInstance = this.ResolveObject(paramType, paramShortName);// 创建参数实例

                // 参数 也要做构造 函数检查
                paramList.Add(paramInstance);
            }
             获取线程ID
            //int threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
            // 反射获取实例 对象
            object instance = Activator.CreateInstance(type, paramList.ToArray());

            // 利用反射获取所有属性,进行注入
            // 属性注入
            foreach (var item in type.GetProperties())
            {
                //item.IsDefined()
                /// 判断是否有特性
                /// 如果有特性就注入   利用ResolveObject方法
            }

            /// 字典-》名称+Type
            /// 进程单例、线程单例
            /// 名称+{Type,线程ID,实例对象列表,实例化类型(枚举:常规、进程单例、线程单例)}
     
     		//动态AOP实现
            ProxyGenerator generator = new ProxyGenerator();// 代理生成器
            MyInterceptor interceptor = new MyInterceptor();
            instance = generator.CreateInterfaceProxyWithTarget(abstractType, instance, interceptor);
            
            return instance;
        }
    }

上端调用:

        static void Main(string[] args)
        {
            IMyIoc myIoc = new MyIoc();
            myIoc.Register<IInfoProcessor, InfoProcessor>();
            IInfoProcessor infoProcessor = myIoc.Resolve<IInfoProcessor>();
            infoProcessor.ReadInfo("Hello AOP");          
        }

Prism.Unity中的AOP

Unity支持三方方式的AOP实现

  • Interface(主流)—适用于BLL业务逻辑层的类,包括接口和实现类
  • VirsualMethod—适用于ViewModel类
  • MarshalByRefObject(单继承,很少用)

需要引用的库:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Microsoft.Xaml.Behaviors.Wpf" version="1.1.19" targetFramework="net48" />
  <package id="Prism.Core" version="8.0.0.1909" targetFramework="net48" />
  <package id="Prism.Unity" version="8.0.0.1909" targetFramework="net48" />
  <package id="Prism.Wpf" version="8.0.0.1909" targetFramework="net48" />
  <package id="System.Runtime.CompilerServices.Unsafe" version="4.5.2" targetFramework="net48" />
  <package id="System.Threading.Tasks.Extensions" version="4.5.2" targetFramework="net48" />
  <package id="Unity.Abstractions" version="5.11.6" targetFramework="net48" />
  <package id="Unity.Configuration" version="5.11.2" targetFramework="net48" />
  <package id="Unity.Container" version="5.11.8" targetFramework="net48" />
  <package id="Unity.Interception" version="5.11.1" targetFramework="net48" />
  <package id="Unity.Interception.Configuration" version="5.11.1" targetFramework="net48" />
</packages>

在这里插入图片描述
定义特性和切面

    public class LogHander : ICallHandler
    {
        public int Order { get; set; }// 执行顺序

        public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
        {
            Console.WriteLine("LogHander方法执行前");

            IMethodReturn methodReturn = getNext().Invoke(input, getNext);

            Console.WriteLine("LogHander方法执行后");

            /// 主要业务逻辑
            /// 还有另外一个切面的话,需要把当前执行过的主业务逻辑传下去
            return methodReturn;

        }
    }

    public class MonitorHandler : ICallHandler
    {
        public int Order { get; set; }

        public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
        {
            Console.WriteLine("MonitorHandler方法执行前");

            IMethodReturn methodReturn = getNext().Invoke(input, getNext);

            Console.WriteLine("MonitorHandler方法执行后");

            return methodReturn;
        }
    }
    public class LogHandlerAttribute : HandlerAttribute
    {
        public override ICallHandler CreateHandler(IUnityContainer container)
        {
            return new LogHander { Order = this.Order };
        }
    }

    public class MonitorHandlerAttribute : HandlerAttribute
    {
        public override ICallHandler CreateHandler(IUnityContainer container)
        {
            return new MonitorHandler { Order = this.Order };
        }
    }

实现的业务逻辑:

    public interface IMenuBll
    {
        [LogHandlerAttribute(Order = 2)]
        [MonitorHandler(Order = 1)]
        void GetMenus();
    }

    public class MenuBll : IMenuBll
    {
        public void GetMenus()
        {
            Console.WriteLine("获取了系统菜单=============");
        }
    }

1、代码配置AOP

        protected override Window CreateShell()
        {
            /// 使用的容器:Unity
            IUnityContainer unityContainer = Container.Resolve<IUnityContainer>();
            // 代码处理   添加一个扩展,扩展用来选择容器,创建对象的时候考虑一下切面
            unityContainer.AddNewExtension<Interception>().RegisterType<IMenuBll, MenuBll>();
            unityContainer.Configure<Interception>().SetInterceptorFor<IMenuBll>(new InterfaceInterceptor());
            
            unityContainer.Resolve<IMenuBll>().GetMenus();
            
            return Container.Resolve<MainWindow>();
        }

2、配置文件配置AOP

Unity.config配置文件

<configuration>
	<configSections>
		<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration"/>
	</configSections>
	<unity>
		<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Unity.Interception.Configuration"/>
		<containers>
			<container name="aopContainer">
				<extension type="Interception"/>
				<register type="My.PrismLesson.Project.BLL.IMenuBll,My.PrismLesson.Project" mapTo="My.PrismLesson.Project.BLL.MenuBll,My.PrismLesson.Project">
					<interceptor type="InterfaceInterceptor"/>
					<interceptionBehavior type="My.PrismLesson.Project.Behavior.LogBeforeBehavior, My.PrismLesson.Project"/>
				</register>
				<register type="My.PrismLesson.Project.ViewModels.LoginWindowViewModel,My.PrismLesson.Project">
					<interceptor type="VirtualMethodInterceptor"/>
					<interceptionBehavior type="My.PrismLesson.Project.Behavior.LogBeforeBehavior, My.PrismLesson.Project"/>
				</register>
				<!--Mash-->
			</container>
		</containers>
	</unity>
</configuration>

需要配置interceptionBehavior节点

    public class LogBeforeBehavior : IInterceptionBehavior
    {
        public bool WillExecute => true;

        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }

        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            MethodInfo methodInfo = input.Target.GetType().GetMethod(input.MethodBase.Name);
			
			//相关的逻辑也可以改成特性中执行,上面的MethodInfo可以获取相应的特性 
            Console.WriteLine("LogBeforeBehavior方法执行前");

            IMethodReturn methodReturn = getNext().Invoke(input, getNext);

            Console.WriteLine("LogBeforeBehavior方法执行后");

            return methodReturn;
        }
    }
        protected override Window CreateShell()
        {
            /// 使用的容器:Unity
            IUnityContainer unityContainer = Container.Resolve<IUnityContainer>();

            // 配置文件
            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
            fileMap.ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "UnityConfig\\Unity.Config");
            Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
            UnityConfigurationSection configSection = (UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);
            configSection.Configure(unityContainer, "aopContainer");

            unityContainer.Resolve<IMenuBll>().GetMenus();
            
            return Container.Resolve<MainWindow>();
        }

3、ViewModel层实现AOP

以上的例子都是针对BLL(业务逻辑层)实现的AOP,只要ViewModel是通过IOC容器来实例化的,那么同样可以对ViewModel来实现AOP的功能。

ViewModel中要加切面的业务逻辑函数要为虚方法(virtual)

    public class LoginWindowViewModel
    {
        public LoginWindowViewModel(Prism.Regions.IRegionManager region)
        {
            /// 如果VM不是由UnityContainer创建的,Region就不可能注入
        }
        private DelegateCommand<Window> _loginCommand;

        public DelegateCommand<Window> LoginCommand
        {
            get
            {
                if (_loginCommand == null)
                    _loginCommand = new DelegateCommand<Window>((w) =>
                    {
                        DoLogin();
                    });
                return _loginCommand;
            }
            set { _loginCommand = value; }
        }

        /// <summary>
        /// 这个方法要为虚方法
        /// </summary>
        public virtual void DoLogin()
        {
            // 登录逻辑执行
            Console.WriteLine("执行登录逻辑");
        }
    }
1)、配置文件配置AOP

Unity.config配置文件,配置interceptor为VirtualMethodInterceptor,采用虚方法注入,所以ViewModel中要加切面的业务逻辑函数要为虚方法(virtual)。

<configuration>
	<configSections>
		<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration"/>
	</configSections>
	<unity>
		<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Unity.Interception.Configuration"/>
		<containers>
			<container name="aopContainer">
				<extension type="Interception"/>
				<register type="My.PrismLesson.Project.ViewModels.LoginWindowViewModel,My.PrismLesson.Project">
					<interceptor type="VirtualMethodInterceptor"/>
					<interceptionBehavior type="My.PrismLesson.Project.Behavior.LogBeforeBehavior, My.PrismLesson.Project"/>
				</register>
				<!--Mash-->
			</container>
		</containers>
	</unity>
</configuration>
2)、代码配置AOP
protected override Window CreateShell()
        {
            /// 使用的容器:Unity
            IUnityContainer unityContainer = Container.Resolve<IUnityContainer>();

            // VM也可以进行代码的方式处理    实操
            // 实战使用Prism -》Unity容器-》日志利用AOP的方式处理  业务逻辑的时候不用管一些公共逻辑
            unityContainer.RegisterType<LoginWindowViewModel>(new Interceptor<VirtualMethodInterceptor>(),new InterceptionBehavior<LogBeforeBehavior>());

            return Container.Resolve<MainWindow>();
        }

用开源AOP简化MVVM框架_君子居易的博客-CSDN博客

本文的前提是知晓基于Xaml开发,本文以WPF为例

 

一 、简化属性通知事件

普通的属性通知会写一个基于INotifyPropertyChanged接口的类

 
  1. public class RasiePropertyChanged : INotifyPropertyChanged
  2. {
  3. public event PropertyChangedEventHandler PropertyChanged;
  4.  
  5. protected void OnPropertyChanged([CallerMemberName]string propertyName = null)
  6. {
  7. PropertyChangedEventHandler handler = PropertyChanged;
  8. if (handler != null)
  9. {
  10. handler(this, new PropertyChangedEventArgs(propertyName));
  11. }
  12. }
  13.  
  14. }
 

这样用时就可以在属性的Set里最后加上一句RasiePropertyChanged();就可以,但是如果属性只是简单的Get,Set写起来也是比较麻烦的

使用Fody/PropertyChanged可省去此麻烦

项目地址:https://github.com/Fody/PropertyChanged

使用方式如下,转自官方

 
  1. [ImplementPropertyChanged]
  2. public class Person
  3. {
  4. public string GivenNames { get; set; }
  5. public string FamilyName { get; set; }
  6.  
  7. public string FullName
  8. {
  9. get
  10. {
  11. return string.Format("{0} {1}", GivenNames, FamilyName);
  12. }
  13. }
  14. }
 

在类上边写上一个Attribute [ImplementPropertyChanged],类里的所有属性就都实现了属性通知事件

DoNotNotify:如果有某个属性不想实现通知事件,就在相应属性上加个[DoNotNotify]

AlsoNotifyFor:如果有某个属性像上边的FullName一样是2个属性的组合,任何一个变化都要通知都FullName变化,就在子属性GivenNames 和FamilyName上加个[AlsoNotifyFor("FullName")]

DependsOn:如果反过来FullName变了也让子属性变化,那就要在FullName上加上[DependsOn("GivenName","FamilyName")]

DoNotSetChanged:这个属性是说当FullName 的Set改变时,不会通知到子属性

DoNotCheckEquality:这个属性是说跳过相等的检查,没有实例,我也没有用过

二、简化ICommand的绑定事件

如果绑定一个Button 的点击事件,正常的后台是写一个DeletedCommand的属性

 
  1. private ICommand _clickCommand;
  2.  
  3. public ICommand ClickCommand
  4. {
  5. get { return _clickCommand ?? new DelegateCommand(Click); }
  6. set { _clickCommand = value; }
  7. }
  8.  
  9. 10 private Action Click()
  10. {
  11. throw new NotImplementedException();
  12. }
 

然后前台绑定这个ClickCommand
使用Fody/Commander.Fody可省去写ICommand的属性

https://github.com/DamianReeves/Commander.Fody

使用方式如下,转自官方

 
  1. [OnCommand("ClickCommand")]
  2. private Action Click()
  3. {
  4. throw new NotImplementedException();
  5. }
 

如此就可以了
但是ICommand接口有2个方法,一个是Execute,一个是CanExecute

所以属性自然也是有2个,分别对应这2个方法OnCommandOnCommandCanExecute

 

如有问题请参照项目说明和示例,本人只是恰巧看到了这2个简单的Fody的项目,简单用一下而已

转载地址

 

搭建Wpf框架(1) —— 管理系统-Wpf客户端框架2.0(OA,聊天,定时任务)

本框架将会持续更新修复,欢迎大家访问。

1.Wpf客户端生成安装包与自动升级包 - 竹天笑 - 博客园 (cnblogs.com)

1.1搭建Wpf框架(2.1) —— Wpf客户端生成安装包与自动升级包2

2.Wpf实现打印报表 - 竹天笑 - 博客园 (cnblogs.com)

3.Wpf使用EFCore操作数据库 - 竹天笑 - 博客园 (cnblogs.com)

4. 搭建Wpf框架(5) —— Wpf使用unity实现AOP - 竹天笑 - 博客园 (cnblogs.com)

5.搭建Wpf框架(6) —— Tile布局控件(可切换布局) - 竹天笑 - 博客园 (cnblogs.com)

6.搭建Wpf框架(7) —— 我的控制台(续6) - 竹天笑 - 博客园 (cnblogs.com)

7搭建Wpf框架(8) —— 3D展示墙 - 竹天笑 - 博客园 (cnblogs.com)

8搭建Wpf框架(9) —— 登录验证控件 - 竹天笑 - 博客园 (cnblogs.com)

9搭建Wpf框架(10) —— 弹出窗口动画 - 竹天笑 - 博客园 (cnblogs.com)

10.搭建Wpf框架(11) —— 多屏窗口

11.搭建Wpf框架(12) —— MahApps2.0.0.0自定义主题

12.一个Wpf控件库(Wpf客户端框架使用) - 竹天笑 - 博客园 (cnblogs.com)

13.为Wpf敏捷开发做准备-Wpf实现Form表单1 - 竹天笑 - 博客园 (cnblogs.com)

14.为Wpf敏捷开发做准备-Wpf实现Form表单2 - 竹天笑 - 博客园 (cnblogs.com)

15.搭建Wpf框架(13) ——代码生成器的使用 - 竹天笑 - 博客园 (cnblogs.com)

16.搭建Wpf框架(14) ——代码生成器的补充(Form表单的拖拽及生成) - 竹天笑 - 博客园 (cnblogs.com)

17.搭建Wpf框架(15) ——敏捷开发crud界面的设计 - 竹天笑 - 博客园 (cnblogs.com)

18.搭建Wpf框架(16) ——敏捷开发crud界面终极版(通过数据库脚本配置,前台无需修改) - 竹天笑 - 博客园 (cnblogs.com)

V.CodeGenerator WPF代码生成器--AOP功能_YCYZ的博客-CSDN博客

前言

前言:
受 WTM 的影响,想自己尝试写一个自动生成WPF项目的代码生成器
本文主要用于介绍基础库中作者自定义的一些<基础服务>的使用。
作者的功底还不是很成熟,请大家多多包涵。


一、引用Vampirewal.Core基础库

详细Nuget引用请点击此处跳转到主介绍页面

二、内容介绍

1、特性

    /// <summary>
    /// VampirewalAop特性基类(需进行AOP拦截操作的方法都需继承自该特性后实现抽象方法)
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public abstract class VampirewalAopAttribute : Attribute
    {
        /// <summary>
        /// 
        /// </summary>
        public bool IsInnerInvoke { set; get; }
        /// <summary>
        /// 执行位置
        /// </summary>
        public InvokeLocation Location { set; get; }

        /// <summary>
        /// 方法执行后
        /// </summary>
        /// <param name="invocation"></param>
        /// <param name="exp"></param>
        public abstract void After(IInvocation invocation, Exception exp);

        /// <summary>
        /// 方法执行中
        /// </summary>
        /// <param name="invocation"></param>
        public abstract void Middle(IInvocation invocation);

        /// <summary>
        /// 方法执行前
        /// </summary>
        public abstract void Before();
    }

2、拦截器

需要自行实现拦截器的哈,需按照这样的方式来写
这个拦截器的作用标记在model类上的特性,用于简化属性通知写法
也可添加都ViewModel上,下文讲使用

    /// <summary>
    /// 属性更新拦截器,用于MVVM
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class AddPropertyNotifyIntercept : Attribute, IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            //正常执行
            invocation.Proceed();

            //获取方法名称
            var methodName = invocation.Method.Name;
            //如果方法名是set开头,那么调用一下属性通知
            if (methodName.StartsWith("set_"))
            {
                var Target = invocation.InvocationTarget as NotifyBase;
                Target.DoNotify(invocation.Method.Name.Substring(4));
            }
        }
    }

原写法:

        private string _Name;
        
        public string Name
        {
            get
            {
                return _Name;
            }
            set
            {
                _Name = value;
                DoNotify();
            }
        }

新写法:

    [AddPropertyNotifyIntercept]
    public class testNotify2 : NotifyBase
    {
        public virtual string Name { get; set; }//切记,此处一定标记为virtual
    }

三、使用

上文已经将 [AddPropertyNotifyIntercept] 特性的一半使用方式讲了,下面讲添加到ViewModel上的情况

    [VampirewalAopIntercept]
    [AddPropertyNotifyIntercept]
    [VampirewalIoCRegister("MainViewModel2", RegisterType.ViewModel)]
    public class MainViewModel2 : BillVM<TestBillModel>
    {
         public virtual testNotify notify { get; set; }

         public RelayCommand testcommand => new RelayCommand(testmethod);

        //继承自 VampirewalAopAttribute 的AOP类,下文讲实现内容
        [TryCatch]
        public virtual void testmethod()
        {
            notify.Name = "bbb";
        }
    }


    [AddPropertyNotifyIntercept]
    public class testNotify:NotifyBase
    {
        public testNotify()
        {

        }
        public testNotify(string name,int age)
        {
            Name=name;
            Age=age;
        }
        public virtual string Name { get; set; }
        public virtual int Age { get; set; }
    }

1、通过上文的方式,在页面创建VM的时候,通过VampirewalIOC创建出来的,就会自动添加属性通知功能
2、[VampirewalAopIntercept]添加了该特性之后,VM中的AOP功能才会启用
3、同时,需要由程序进行属性通知的属性,都需要virtual

TryCatch案例:

    /// <summary>
    /// 通过AOP的方式执行TryCatch(案例)
    /// </summary>
    public class TryCatchAttribute : VampirewalAopAttribute
    {
        public TryCatchAttribute()
        {
            this.IsInnerInvoke = true;
        }

        public override void After(IInvocation invocation, Exception exp)
        {
            if (exp != null)
            {
                Debug.WriteLine("处理错误,错误信息为:" + exp.InnerException.Message ?? exp.Message);
            }
        }

        public override void Before()
        {
            Debug.WriteLine("开始捕捉异常");
        }

        public override void Middle(IInvocation invocation)
        {
            try
            {
                invocation.Proceed();
            }
            catch (Exception exp)
            {
                Debug.WriteLine(string.IsNullOrEmpty(exp.Message) ? exp.InnerException.Message : exp.Message);
            }
        }
    }

 

posted @ 2023-05-11 17:52  CharyGao  阅读(88)  评论(0)    收藏  举报