1. 前言 介绍系统分层模型、如何安装Prism快速开发模板与MVVM框架使用、如何配置ORM框架Entity Framework Core与使用、以及Postgresql数据库配置。
2. 系统分层 项目比较简单,大概分层模型如下:
View双向绑定ViewModel;
ViewModel调用Service取得DataModel业务数据;
Service通过调用Repository取得Entity数据;
Repository调用Entity Framework Core,自动创建Sql执行并返回Entity对象;
Entity Framework Core通过驱动链接数据库。
如果项目功能或者对接端末比较多,最好扩展成微服务。
3. MVVM框架之Prism MVVM(Model–view–viewmodel)是微软的WPF和Silverlight架构师之一John Gossman于2005年发布的软件架构模式。目的就是把用户界面设计与业务逻辑开发分离,方便团队开发和自动化测试。
架构的设计目标永远都是高内聚低耦合,当然架构的设计不免会带来代码量的”增加”,但是带来的好处很多:扩展性高/复用度高/易修改/可读性高。MVVM优点:
低耦合:视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变
可重用性:你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑
独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xml代码
可测试:界面素来是比较难于测试的,而现在测试可以针对ViewModel来写
3.1、无框架的MVVM实现 设计与逻辑分离的基本就是绑定,通过发布者订阅者模式实现数据更新通知。
3.1.1、属性绑定 默认属性为单向绑定,如果需要双向绑定需要实现INotifyPropertyChanged接口。
第一步:一般是建立如下基类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 using System;using System.ComponentModel;using System.Runtime.CompilerServices;namespace MvvmDemo.Common { public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void NotifyPropertyChanged ([CallerMemberName] String propertyName = "" ) { if (PropertyChanged != null ) { PropertyChanged(this , new PropertyChangedEventArgs(propertyName)); } } } }
第二步:各个ViewModel继承基类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class UserViewModel : ViewModelBase { private string _userId; private string _userName; public string UserId { get { return _userId; } set { _userId = value ; NotifyPropertyChanged(); } } public string UserName { get { return _userName; } set { _userName = value ; NotifyPropertyChanged(); } } }
第三步:Xaml绑定属性,实现消息通知。
1 2 < TextBox Text = "{Binding UserID,Mode=TwoWay}" /> < TextBox Grid . Row = "1" Text = "{Binding UserName,Mode=OneWay}" />
备注:通过IValueConverter可以做一些特殊绑定处理。比如,经典的就是Bool值控制Visibility。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 [ValueConversion(typeof(bool), typeof(Visibility)) ] public class BoolToVisibiltyConverter : MarkupExtension , IValueConverter { public object Convert (object value , Type targetType, object parameter, CultureInfo culture ) { bool flag = false ; if (value is bool ) { flag = (bool )value ; } else if (value is bool ?) { bool ? nullable = (bool ?)value ; flag = nullable.HasValue ? nullable.Value : false ; } return (flag ? Visibility.Visible : Visibility.Collapsed); } public object ConvertBack (object value , Type targetType, object parameter, CultureInfo culture ) { return value ; } public override object ProvideValue (IServiceProvider serviceProvider ) { return this ; } }
Xaml绑定:头部需要引入命名空间。
xmlns:converter="clr-namespace:WpfMvvm.Core.Converters"
1 2 3 4 5 <Button Grid.Row= "2" Visibility= "{Binding ShowFlg,Converter={converter:BoolToVisibiltyConverter}}" Command= "{Binding AddCmd}" Content= "登录" />
3.1.2、事件绑定 WPF提供了Command事件处理属性,想利用控件中的Command属性需要实现了ICommand接口的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Windows.Input;namespace MvvmDemo.Common { public class DelegateCommand <T >: ICommand { private Action<T> _Command; private Func<T, bool > _CanExecute; public event EventHandler CanExecuteChanged; public DelegateCommand (Action<T> command ):this (command,null ) { } public DelegateCommand (Action<T> command,Func<T,bool > canexecute ) { if (command==null ) { throw new ArgumentException("command" ); } _Command = command; _CanExecute = canexecute; } public bool CanExecute (object parameter ) { return _CanExecute == null ? true : _CanExecute((T)parameter); } public void Execute (object parameter ) { _Command((T)parameter); } } }
使用它作为事件属性的类型就可以了。
1 2 3 4 5 6 7 8 9 10 public DelegateCommand <string > LoginCommand => new DelegateCommand <string >( s => { }, s => !string .IsNullOrEmpty (s) );
3.2、Prism的MVVM实现 至于Prism有很多种理由让我选择它,比如:
支持MVVM(Binding、Notification、Command等)、微软成员维护
支持Unity和DryIoc两种IOC容器
支持WPF、UWP、Xamarin.Froms开发
封装界面跳转
封装弹出消息框
自带项目模板与快速开发代码片段
创建View时自动创建ViewModel
默认自动绑定ViewModel到View
…等等
3.2.1、配置Prism 最简单的方法:安装Prism Template Pack扩展包。
3.2.2、使用Prism 通过Prism项目模板创建项目,目前可以创建WPF(.Net Framework和.Net Core)、UWP、Xamarin.Froms等应用。
以前支持四种容器,现在只支持两种IOC容器:Unity、DryIoc。
*备注:如果通过Prism模板创建项目时出现以下错误:
这是因为Autofac已经不被支持。解决办法:regedit进入注册表HKEY_CURRENT_USER\Software\Prism,把SelectedContainer删除或者改成Unity。
生成的解决方案如下:
亮点:解决方案中自动设置了ViewModel的IOC配置,MainWindow.xaml中ViewModel的绑定也自动设置了。
下面通过建立一个简单的局部界面跳转实例,体验一把Prism的高效率:cmd、propp、vs智能提示。
Prism包提供的代码片段如下,要好好加以利用:
此次项目还用到了以下特性:
3.2.2.1 Region Navigation
局部页面跳转:
传递对象参数;
跳转前确认;
自定义如何处理已经显示过的页面(覆盖与否);
通过IRegionNavigationJournal接口可以操作页面跳转履历(返回与前进等)。
如上例所示简单应用。
第一步:标识显示位置。
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
第二步:在App.xaml.cs注册跳转页面。
1 2 3 4 5 6 7 8 9 10 11 12 public partial class App { protected override Window CreateShell () { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes (IContainerRegistry containerRegistry ) { containerRegistry.RegisterForNavigation<PageTwo, PageTwoViewModel>(); } }
第三步:使用IRegionManager实现跳转。
1 2 _manager .RequestNavigate("ContentRegion" , "PageTwo" );
3.2.2.2、Modules
如果系统功能比较多最好进行分块处理,如下面订单和用户信息的分块处理。
App.xaml.cs中统一各个模块数据。
1 2 3 4 5 6 protected override void ConfigureModuleCatalog (IModuleCatalog moduleCatalog) { moduleCatalog.AddModule <OrderModule>(); moduleCatalog.AddModule <CustomerModule>(); }
各个Module里面还是一样,使用到的所有Service和Repository都注册,使用IOC容器进行生命周期管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class OrderModule : IModule { public void OnInitialized(IContainerProvider containerProvider) { } public void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigation<MainWin>(PageDefine.Order); containerRegistry.Register<ISearchService, SearchService>(); } }
3.2.2.3、Dialog Service
自定义消息弹出框,比如警告、错误、提示等消息框。
第一步:自定义消息框控件,ViewModel继承IDialogAware接口并实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class NotificationDialogViewModel : BindableBase , IDialogAware { private DelegateCommand<string > _closeDialogCommand; public DelegateCommand<string > CloseDialogCommand => _closeDialogCommand ?? (_closeDialogCommand = new DelegateCommand<string >(CloseDialog)); private string _message; public string Message { get { return _message; } set { SetProperty(ref _message, value ); } } private string _title = "Notification" ; public string Title { get { return _title; } set { SetProperty(ref _title, value ); } } public event Action<IDialogResult> RequestClose; protected virtual void CloseDialog (string parameter ) { ButtonResult result = ButtonResult.None; if (parameter?.ToLower() == "true" ) result = ButtonResult.OK; else if (parameter?.ToLower() == "false" ) result = ButtonResult.Cancel; RaiseRequestClose(new DialogResult(result)); } 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 ) { Message = parameters.GetValue<string >("message" ); } }
第二步:App.xaml.cs中注册自定义的消息框,从而覆盖默认的消息框:
1 2 3 4 5 6 7 8 9 10 11 12 public partial class App { protected override Window CreateShell () { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes (IContainerRegistry containerRegistry ) { containerRegistry.RegisterDialog<NotificationDialog, NotificationDialogViewModel>(); } }
第三步:通过IDialogService使用消息框:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void ShowDialog () { var message = "This is a message that should be shown in the dialog." ; _dialogService.ShowDialog("NotificationDialog" , new DialogParameters($"message={message} " ), r => { if (r.Result == ButtonResult.None) Title = "Result is None" ; else if (r.Result == ButtonResult.OK) Title = "Result is OK" ; else if (r.Result == ButtonResult.Cancel) Title = "Result is Cancel" ; else Title = "I Don't know what you did!?" ; }); }
第四步:定义消息框显示属性:
1 2 3 4 5 6 7 8 < 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 >
其他用法可以参照Prism开源库:https://github.com/PrismLibrary/Prism
4. Entity Framework Core + Postgresql EntityFrameworkCore:是对象关系映射(ORM)程序,支持语言集成查询Linq,是轻量、可扩展、开源跨平台的数据访问框架。下一个5.0版本将与.NET 5.0一起发布。EntityFrameworkCore只支持CodeFirst,EntityFramework支持DB First和Code First。之所以选择EFCore是因为:
支持CodeFirst
支持Linq
双向映射(linq映射成sql,结果集映射成对象)
速度很快
PostgreSQL:是开源先进的对象-关系型数据库管理系统(ORDBMS),有些特性甚至连商业数据库都不具备。支持JSON数据存储,表之间还可以继承。
4.1、配置EFCore与PostgreSQL 4.1.1、引入针对PostgreSQL的EFCore包
4.1.2、添加DB操作上下文 数据库链接替换为你的链接,一般都是放配置文件管理。
添加Users字段,通过EFCore将自动创建Users表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 using System ;using Microsoft.EntityFrameworkCore;using WpfMccm.Entitys;namespace WpfMvvm.DataAccess { public class UserDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql("Server=127.0.0.1;Database=HBMCS;Port=5432;User Id=test;Password=test;Ssl Mode=Prefer;", npgsqlOptionsAction: options => { options .CommandTimeout(60 ); options .EnableRetryOnFailure(maxRetryCount: 5 , maxRetryDelay: TimeSpan.FromSeconds(30 ), errorCodesToAdd: null ); }); } public DbSet<User > Users { get ; set ; } } }
CodeFirst必备神器。进入程序包管理器控制台,输入以下命名安装EFCore设计工具:
※必须安装在启动项目里面,不然会失败。
Install-Package Microsoft.EntityFrameworkCore.Tools
4.1.4、创建Migration 程序包管理器控制台,默认项目一定要选择DB操作上下文的项目,然后执行命令:InitDB是文件区分,可以任意修改。
Add-Migration InitDB
4.1.5、生成数据库脚本(生产阶段用,开发阶段可跳过) 程序包管理器控制台,执行如下命令生成SQL脚本文件:
Script-Migration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( "MigrationId" character varying (150 ) NOT NULL , "ProductVersion" character varying (32 ) NOT NULL , CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") ); CREATE TABLE "Users" ( "ID" integer NOT NULL GENERATED BY DEFAULT AS IDENTITY , "Name" text NULL , "Age" integer NOT NULL , CONSTRAINT "PK_Users" PRIMARY KEY ("ID") ); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")VALUES ('20200413133616_InitDB' , '3.1.3' );
如果系统已经上线,安全起见则需要使用这个方法生成SQL脚本,手动执行SQL更新数据库。
4.1.6、更新数据库(开发阶段用) 程序包管理器控制台,执行如下命令将表定义更新到DB(按文件名的时间顺序添加):
这样我们就通过类创建了一个数据库表Users,同时默认会在__EFMigrationsHistory履历表添加一条合并记录。
※如果__EFMigrationsHistory中记录存在则忽略本次更新。
4.2、使用DB上下文操作数据库 4.2.1、创建IRepository,DB操作基本接口 1 2 3 4 5 public interface IRepository <TEntity > where TEntity : class { Task<TEntity> GetAsync(int id); Task<bool > AddAsync(TEntity obj); }
4.2.2、创建UserRepository,User专用的DB操作类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class UserRepository : IRepository <User > { private readonly DbContext _dbContext; private readonly DbSet<User> _dbSet; public UserRepository (UserDbContext dbContext ) { _dbContext = dbContext; _dbSet = dbContext.Set<User>(); } public async Task<bool > AddAsync (User obj ) { _dbSet.Add(obj); return await _dbContext.SaveChangesAsync() > 0 ; } public async Task<User> GetAsync (int id ) { return await _dbSet.FindAsync(id); } }
如果需要进行事务操作,可以使用下面方法:
1 2 var tran= _dbContext.Database.BeginTransaction() tran.Commit()
4.2.3、Service层调用UserRepository就可以完成用户的操作。 5. 总结 整体来说Prism简化了应用的设计与架构,EFCore简化了数据库操作。