1. 程式人生 > >Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 終極篇

Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 終極篇

 本著每天記錄一點成長一點的原則,打算將目前完成的一個WPF專案相關的技術分享出來,供團隊學習與總結。

總共分三個部分:

基礎篇主要針對C#初學者,鞏固C#常用知識點;

中級篇主要針對WPF佈局與MaterialDesign美化設計,在減輕程式碼量的情況做出漂亮的應用;

終極篇為框架應用實戰,包含系統分層、MVVM框架Prism安裝與使用、ORM框架EntityFramework Core配置與使用、開源資料庫Postgresql配置與使用。

目錄

  1. Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 基礎篇
  2. Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 中級篇
  3. Prism+MaterialDesign+EntityFramework Core+Postgresql WPF開發總結 之 終極篇

前言

此篇主要介紹系統分層模型、如何安裝Prism快速開發模板與MVVM框架使用、如何配置ORM框架Entity Framework Core與使用、以及Postgresql資料庫配置。

系統分層

專案比較簡單,大概分層模型如下:

  1. View雙向繫結ViewModel;
  2. ViewModel呼叫Service取得DataModel業務資料;
  3. Service通過呼叫Repository取得Entity資料;
  4. Repository呼叫Entity Framework Core,自動建立Sql執行並返回Entity物件;
  5. 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));
            }
        }
    }
}
View Code

第二步:各個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();
            }
        }
    }
View Code

第三步:Xaml繫結屬性,實現訊息通知。

        <TextBox Text="{Binding UserID,Mode=TwoWay}" />
        <TextBox Grid.Row="1" Text="{Binding UserName,Mode=OneWay}" />
View Code

備註:通過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;
        }
    }
View Code

Xaml繫結:頭部需要引入名稱空間。

xmlns:converter="clr-namespace:WpfMvvm.Core.Converters"
        <Button
            Grid.Row="2"
            Visibility="{Binding ShowFlg,Converter={converter:BoolToVisibiltyConverter}}"
            Command="{Binding AddCmd}"
            Content="登入" />
View Code

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);
        }
    }
}
View Code

使用它作為事件屬性的型別就可以了。

        /// <summary>
        /// 登陸命令
        /// </summary>
        public DelegateCommand<string> LoginCommand => new DelegateCommand<string>(
                        s =>
                        {
                            // todo
                        },
                        s => !string.IsNullOrEmpty(s)
                        );
View Code

二、Prism的MVVM實現

至於Prism有很多種理由讓我選擇它,比如:

  1. 支援MVVM(Binding、Notification、Command等)、微軟成員維護
  2. 支援Unity和DryIoc兩種IOC容器
  3. 支援WPF、UWP、Xamarin.Froms開發
  4. 封裝介面跳轉
  5. 封裝彈出訊息框
  6. 自帶專案模板與快速開發程式碼片段
  7. 建立View時自動建立ViewModel
  8. 預設自動繫結ViewModel到View
  9. ...等等

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註冊跳轉頁面。

    public partial class App
    {
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterForNavigation<PageTwo, PageTwoViewModel>();
        }
    }
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>();
        }
View Code

各個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>();
        }
    }
View Code

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");
        }
    }
View Code

第二步: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>();
        }
    }
View Code

第三步:通過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!?";
            });
        }
View Code

第四步:定義訊息框顯示屬性:

    <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>
View Code

其他用法可以參照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; }
    }
}
View Code

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');
View Code

如果系統已經上線,安全起見則需要使用這個方法生成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);
    }
View Code

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);
        }
    }
View Code

如果需要進行事務操作,可以使用下面方法:

           var tran= _dbContext.Database.BeginTransaction();
           tran.Commit();
View Code

3、Service層呼叫UserRepository就可以完成使用者的操作。

總結

此篇量稍微有點多,非常感謝能看到這裡。整體來說Prism簡化了應用的設計與架構,EFCore簡化了資料庫操作。

&n