本着每天記錄一點成長一點的原則,打算將目前完成的一個WPF項目相關的技術分享出來,供團隊學習與總結。
總共分三個部分:
基礎篇主要針對C#初學者,鞏固C#常用知識點;
中級篇主要針對WPF布局與MaterialDesign美化設計,在減輕代碼量的情況做出漂亮的應用;
終極篇為框架應用實戰,包含系統分層、MVVM框架Prism安裝與使用、ORM框架EntityFramework Core配置與使用、開源數據庫Postgresql配置與使用。
目錄
- Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 基礎篇
- Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 中級篇
- Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 終極篇
前言
此篇主要介紹系統分層模型、如何安裝Prism快速開發模板與MVVM框架使用、如何配置ORM框架Entity Framework Core與使用、以及Postgresql數據庫配置。
系統分層
項目比較簡單,大概分層模型如下:
- View雙向綁定ViewModel;
- ViewModel調用Service取得DataModel業務數據;
- Service通過調用Repository取得Entity數據;
- Repository調用Entity Framework Core,自動創建Sql執行並返回Entity對象;
- Entity Framework Core通過驅動鏈接數據庫。

如果項目功能或者對接端末比較多,最好擴展成微服務。

MVVM框架之Prism
MVVM(Model–view–viewmodel)是微軟的WPF和Silverlight架構師之一John Gossman於2005年發布的軟件架構模式。目的就是把用戶界面設計與業務邏輯開發分離,方便團隊開發和自動化測試。目前流行的Android開發、Web開發都在使用,具體MVVM的介紹參照個人博客:核心框架MVVM與MVC、MVP的區別(圖文詳解)。
一、無框架的MVVM實現
設計與邏輯分離的基本就是綁定,通過發布者訂閱者模式實現數據更新通知。
1、屬性綁定
默認屬性為單向綁定,如果需要雙向綁定需要實現INotifyPropertyChanged接口。
第一步:一般是建立如下基類。
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MvvmDemo.Common
{
/// <summary>
/// Viewmodel基類,屬性雙向綁定基礎
/// </summary>
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// 屬性變更通知
/// </summary>
/// <param name="propertyName">屬性名</param>
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
第二步:各個ViewModel繼承基類。
public class UserViewModel : ViewModelBase
{
private string _userId;
private string _userName;
/// <summary>
/// 用戶名
/// </summary>
public string UserId
{
get
{
return _userId;
}
set
{
_userId = value;
NotifyPropertyChanged();
}
}
/// <summary>
/// 用戶名
/// </summary>
public string UserName
{
get
{
return _userName;
}
set
{
_userName = value;
NotifyPropertyChanged();
}
}
}
第三步:Xaml綁定屬性,實現消息通知。
<TextBox Text="{Binding UserID,Mode=TwoWay}" />
<TextBox Grid.Row="1" Text="{Binding UserName,Mode=OneWay}" />
備注:通過IValueConverter可以做一些特殊綁定處理。比如,經典的就是Bool值控制Visibility。
[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"
<Button
Grid.Row="2"
Visibility="{Binding ShowFlg,Converter={converter:BoolToVisibiltyConverter}}"
Command="{Binding AddCmd}"
Content="登錄" />
2、事件綁定
WPF提供了Command事件處理屬性,想利用控件中的Command屬性需要實現了ICommand接口的屬性。
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
{
/// <summary>
/// 命令
/// </summary>
private Action<T> _Command;
/// <summary>
/// 命令可否執行判斷
/// </summary>
private Func<T, bool> _CanExecute;
/// <summary>
/// 可執行判斷結束后通知命令執行
/// </summary>
public event EventHandler CanExecuteChanged;
/// <summary>
/// 構造函數
/// </summary>
/// <param name="command">命令</param>
public DelegateCommand(Action<T> command):this(command,null)
{
}
/// <summary>
/// 構造函數
/// </summary>
/// <param name="command">命令</param>
/// <param name="canexecute">命令可執行判斷</param>
public DelegateCommand(Action<T> command,Func<T,bool> canexecute)
{
if(command==null)
{
throw new ArgumentException("command");
}
_Command = command;
_CanExecute = canexecute;
}
/// <summary>
/// 命令執行判斷
/// </summary>
/// <param name="parameter">判斷數據</param>
/// <returns>判定結果(True:可執行,False:不可執行)</returns>
public bool CanExecute(object parameter)
{
return _CanExecute == null ? true : _CanExecute((T)parameter);
}
/// <summary>
/// 執行命令
/// </summary>
/// <param name="parameter">參數</param>
public void Execute(object parameter)
{
_Command((T)parameter);
}
}
}
使用它作為事件屬性的類型就可以了。
/// <summary>
/// 登陸命令
/// </summary>
public DelegateCommand<string> LoginCommand => new DelegateCommand<string>(
s =>
{
// todo
},
s => !string.IsNullOrEmpty(s)
);
二、Prism的MVVM實現
至於Prism有很多種理由讓我選擇它,比如:
- 支持MVVM(Binding、Notification、Command等)、微軟成員維護
- 支持Unity和DryIoc兩種IOC容器
- 支持WPF、UWP、Xamarin.Froms開發
- 封裝界面跳轉
- 封裝彈出消息框
- 自帶項目模板與快速開發代碼片段
- 創建View時自動創建ViewModel
- 默認自動綁定ViewModel到View
- ...等等
1、配置Prism
最簡單的方法:安裝Prism Template Pack擴展包。

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包提供的代碼片段如下,要好好加以利用:

此次項目還用到了以下特性:
2.1 Region Navigation
局部頁面跳轉:
- 傳遞對象參數;
- 跳轉前確認;
- 自定義如何處理已經顯示過的頁面(覆蓋與否);
- 通過IRegionNavigationJournal接口可以操作頁面跳轉履歷(返回與前進等)。
如上例所示簡單應用。
第一步:標識顯示位置。
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
第二步:在App.xaml.cs注冊跳轉頁面。
View Code
第三步:使用IRegionManager實現跳轉。
// 指定需要顯示的頁面名字與顯示位置的ContentControl的名字
_manager.RequestNavigate("ContentRegion", "PageTwo");
2.2、Modules
如果系統功能比較多最好進行分塊處理,如下面訂單和用戶信息的分塊處理。

App.xaml.cs中統一各個模塊數據。
// ModuleLoader會把各個模塊的IOC依賴注入數據匯總共有管理
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<OrderModule>();
moduleCatalog.AddModule<CustomerModule>();
}
各個Module里面還是一樣,使用到的所有Service和Repository都注冊,使用IOC容器進行生命周期管理。
public class OrderModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<MainWin>(PageDefine.Order);
containerRegistry.Register<ISearchService, SearchService>();
}
}
2.3、Dialog Service
自定義消息彈出框,比如警告、錯誤、提示等消息框。
第一步:自定義消息框控件,ViewModel繼承IDialogAware接口並實現:
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中注冊自定義的消息框,從而覆蓋默認的消息框:
public partial class App
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterDialog<NotificationDialog, NotificationDialogViewModel>();
}
}
第三步:通過IDialogService使用消息框:
private void ShowDialog()
{
var message = "This is a message that should be shown in the dialog.";
//using the dialog service as-is
_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!?";
});
}
第四步:定義消息框顯示屬性:
<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
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數據存儲,表之間還可以繼承。
一、配置EFCore與PostgreSQL
※PostgreSQL安裝參照個人博客:【Windows】PostgreSql安裝
1、引入針對PostgreSQL的EFCore包

2、添加DB操作上下文
數據庫鏈接替換為你的鏈接,一般都是放配置文件管理。
添加Users字段,通過EFCore將自動創建Users表。
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; }
}
}
3、安裝Microsoft.EntityFrameworkCore.Tools工具
CodeFirst必備神器。進入程序包管理器控制台,輸入以下命名安裝EFCore設計工具:
※必須安裝在啟動項目里面,不然會失敗。
Install-Package Microsoft.EntityFrameworkCore.Tools

4、創建Migration
程序包管理器控制台,默認項目一定要選擇DB操作上下文的項目,然后執行命令:InitDB是文件區分,可以任意修改。
Add-Migration InitDB
執行成功之后,生成帶InitDB區分的表定義數據文件:

6、生成數據庫腳本(生產階段用,開發階段可跳過)
程序包管理器控制台,執行如下命令生成SQL腳本文件:
Script-Migration
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更新數據庫。
7、更新數據庫(開發階段用)
程序包管理器控制台,執行如下命令將表定義更新到DB(按文件名的時間順順添加):
Update-Database
這樣我們就通過類創建了一個數據庫表Users,同時默認會在__EFMigrationsHistory履歷表添加一條合並記錄。
※如果__EFMigrationsHistory中記錄存在則忽略本次更新。
二、使用DB上下文操作數據庫
1、創建IRepository,DB操作基本接口
public interface IRepository<TEntity> where TEntity : class
{
Task<TEntity> GetAsync(int id);
Task<bool> AddAsync(TEntity obj);
}
2、創建UserRepository,User專用的DB操作類
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);
}
}
如果需要進行事務操作,可以使用下面方法:
var tran= _dbContext.Database.BeginTransaction();
tran.Commit();
3、Service層調用UserRepository就可以完成用戶的操作。
總結
此篇量稍微有點多,非常感謝能看到這里。整體來說Prism簡化了應用的設計與架構,EFCore簡化了數據庫操作。

