AppBoxCore - 細粒度權限管理框架(EFCore+RazorPages+async/await)!


目錄

  1. 前言
  2. 全新AppBoxCore
    1. RazorPages 和 TagHelpers 技術架構
    2. 頁面處理器和數據庫操作的異步調用
    3. Authorize特性和自定義權限驗證過濾器
      1. Authorize登錄授權
      2. 自定義CheckPower權限過濾器
      3. CheckPower特性控制頁面的瀏覽權限
      4. 表格行鏈接圖標的權限控制
      5. 表格行刪除按鈕的后台權限控制
    4. 實體類模型定義的多對多聯接表
      1. 為什么 EF Core 不支持隱式聯接表
      2. 定義聯接表模型類
      3. 配置多對多關系
      4. 聯接表相關代碼更新
      5. 新增 IKey2ID 接口
    5. 表單和表格的快速模型初始化
      1. 表單控件的快速模型初始化
      2. 表格控件的快速模型初始化
    6. 對比 Dapper 和 EFCore 的實現細節
      1. 角色列表頁面
      2. 向角色中添加用戶列表
      3. 編輯用戶
      4. Menu模型類的ViewPowerName屬性
        1. 編輯頁面(獲取初始數據)
        2. 列表頁面
        3. 編輯頁面
        4. 編輯頁面后台   
  3. 截圖賞析
    1. 深色主題(Dark Hive)
    2. 淺色主題(Pure Purple)  
  4. 源代碼下載

 

 

一、前言

AppBox的歷史可以追溯到 2009 年,第一個版本的 AppBox 是基於 FineUI(開源版)的通用權限管理系統,包括用戶管理、職稱管理、部門管理、角色管理、角色權限管理等模塊。

 

AppBox提供一種通用的細粒度的權限控制結構,可以對頁面上任意元素(按鈕,文本框,表格行中的鏈接)的啟用禁用顯示隱藏進行單獨的控制。

AppBox中的權限管理涉及幾個概念:角色、用戶、權限、頁面

  1. 角色:用來對用戶進行分組,權限實際上是和角色對應的
  2. 用戶:一個用戶可以屬於多個角色
  3. 權限:頂級權限列表,比如“CoreDeptView”的意思是部門瀏覽權限,為了方便權限管理,我們還給權限一個簡單的分組
  4. 頁面:用戶操作的載體,一個頁面可以擁有多個權限,這個控制是在頁面代碼中進行的,主動權在頁面

 這也是我們在 AppBox v3.0 中率先提出的【扁平化的權限設計】理念,用一張圖來概括:

 

一路走來,我們累計了好多篇文章,這里一並匯總出來:

2020年:

....

2018年:

【續】【AppBox】5年后,我們為什么要從 Entity Framework 轉到 Dapper 工具?  

【AppBox】5年后,我們為什么要從 Entity Framework 轉到 Dapper 工具?

【視頻教程】一步步將AppBox升級到Pro版

2016年:

AppBox v6.0中實現子頁面和父頁面的復雜交互  

2014年:

AppBoxPro - 細粒度通用權限管理框架(可控制表格行內按鈕)源碼提供下載  

【6年開源路】FineUI家族今日全部更新(FineUI + FineUI3to4 + FineUI.Design + AppBox)! 

2013年:

AppBox_v2.0完整版免費下載,暨AppBox_v3.0正式發布!

AppBox升級進行時 - 擁抱Entity Framework的Code First開發模式

AppBox升級進行時 - 扁平化的權限設計

AppBox升級進行時 - Entity Framework的增刪改查

AppBox升級進行時 - 如何向OrderBy傳遞字符串參數(Entity Framework)

AppBox升級進行時 - 關聯表查詢與更新(Entity Framework)

AppBox升級進行時 - Attach陷阱(Entity Framework)

AppBox升級進行時 - Any與All的用法(Entity Framework)

AppBox - From Subsonic to EntityFramework 

2012年:

AppBox v1.0 發布了 

AppBox v2.0 發布了! 

2010年:

AppBox - 企業綜合管理系統框架最新進展

2009年:

ExtAspNet應用技巧(二十四) - AppBox之Grid數據庫分頁排序與批量刪除

 

二、全新AppBoxCore 

為什么稱為全新 AppBoxCore?因為這次的升級我們采用了微軟最新的跨平台 .Net Core 3.1 版本,所有技術架構都是引領潮流的存在:

  • 基於最新的 FineUICore 控件庫
  • 基於  ASP.NET Core 的 RazorPages 和 TagHelpers 技術架構
  • 使用 Entity Framework Core 進行數據庫檢索和更新
  • 頁面處理器(GET/POST)和數據庫操作全部改為異步調用(async/await)。
  • 基於頁面模型的Authorize特性和自定義權限驗證過濾器CheckPowerAttribute
  • 實體類模型定義的多對多聯接表(RoleUser)
  • 使用依賴注入添加數據庫連接實例

 

2.1 RazorPages 和 TagHelpers 技術架構

Razor Pages 和 Tag Helpers 是微軟在 ASP.NET Core 中的創新,使得傳統的 MVC 架構在文件組織和頁面標簽上更像傳統的 ASP.NET WebForms,並且使用更加簡單。

我曾在 2009年寫過一篇文章,介紹引用這兩個特性的 FineUICore 看起來和之前的WebForms版本有多類似,可以參考一下:

【FineUICore】全新ASP.NET Core,比WebForms還簡單!  

下面就以這篇文章中的一張經典對比截圖看下兩者有多類似:

 

在官網的更新記錄(FineUICore v5.5.0),我們給出了這樣的文字描述

+支持ASP.NET Core的新特性Razor Pages和Tag Helpers。
    -重寫了全部在線示例(包含750多個頁面),訪問網址:https://pages.fineui.com/
    +Razor Pages相比之前的Model-View-Controller,有如下優點:
        -Razor Pages是創建ASP.NET Core 2.0+網站應用程序的推薦方法。
        -Razor Pages基於文件夾的組織結構,無需復雜的路由配置和額外引入的Areas概念。
        -Razor Pages將MVC的Controller,Action和ViewModel合並為一個PageModel,更加輕量級。
        -Razor Pages的Page和PageModel在一個文件夾下,而MVC的Controller和View分別在不同的文件夾下,並且View還是二級目錄。
        -Razor Pages中Page和PageModel一一對應,避免MVC下可能出現的巨大Controller現象(一個Controller對應多個視圖)。
        -Razor Pages默認設置更安全,無需為每一個控制器方法指定ValidateAntiForgeryToken特性。
    +Tag Helpers相比之前的Html Helpers,有如下優點:
        -Tag Helpers是創建Razor Views和Razor Pages的推薦方法。
        -Tag Helpers更像是標准的HTML,熟悉HTML的前端設計師,無需學習C# Razor語法即可編輯視圖或頁面。
        -Tag Helpers可以更好地配合VS的智能感知,在你輸入標簽的第一個字符開始就提供強大的代碼輔助完成功能。
        -Tag Helpers更容易被WebForms開發人員所接受,可以直接從WebForms項目中拷貝頁面標簽到ASP.NET Core視圖中。
        -Tag Helpers可以更好地配合VS的文檔格式化工具(Ctrl+K, D),而Html Helpers在VS中格式化會有無限縮進的問題。

 

毫無疑問,如果你還在從事 ASP.NET WebForms 的相關開發,並希望學習微軟的最新 ASP.NET Core 技術的話,這次的 AppBoxCore 將是最佳的學習案例!

 

下面給出 AppBoxCore 中的登錄頁面標簽,是不是似曾相識:

<f:Window ID="Window1" IsModal="true" Hidden="false" EnableClose="false" EnableMaximize="false" WindowPosition="GoldenSection" Icon="Key" Title="@Model.Window1Title" Layout="HBox" BoxConfigAlign="Stretch" BoxConfigPosition="Start" Width="500">
    <Items>
        <f:Image ID="imageLogin" ImageUrl="~/res/images/login/login_2.png" CssClass="login-image">
        </f:Image>
        <f:SimpleForm ID="SimpleForm1" LabelAlign="Top" BoxFlex="1" BodyPadding="30 20" ShowBorder="false" ShowHeader="false">
            <Items>
                <f:TextBox ID="tbxUserName" FocusOnPageLoad="true" Label="帳號" Required="true" ShowRedStar="true" Text="">
                </f:TextBox>
                <f:TextBox ID="tbxPassword" TextMode="Password" Required="true" ShowRedStar="true" Label="密碼" Text="">
                </f:TextBox>
            </Items>
        </f:SimpleForm>
    </Items>
    <Toolbars>
        <f:Toolbar Position="Bottom">
            <Items>
                <f:ToolbarText Text="管理員賬號: admin/admin"></f:ToolbarText>
                <f:ToolbarFill></f:ToolbarFill>
                <f:Button ID="btnSubmit" Icon="LockOpen" Type="Submit" ValidateForms="SimpleForm1" OnClickFields="SimpleForm1" OnClick="@Url.Handler("btnSubmit_Click")" Text="登陸"></f:Button>
            </Items>
        </f:Toolbar>
    </Toolbars>
</f:Window>

 

2.2 頁面處理器和數據庫操作的異步調用

服務器的可用線程是有限的,在高負載情況下的可能所有線程都被占用,此時服務器就無法處理新的請求,直到有線程被釋放。

  • 使用同步代碼時,可能會出現多個線程被占用而不能執行任何操作的情況,因為它們正在等待 I/O 完成。
  • 使用異步代碼時,當線程正在等待 I/O 完成時,服務器可以將其線程釋放用於處理其他請求。

下面就以角色編輯頁面,異步代碼調用如下:

[BindProperty]
public Role Role { get; set; }

public async Task<IActionResult> OnGetAsync(int id)
{
    Role = await DB.Roles
        .Where(m => m.ID == id).AsNoTracking().FirstOrDefaultAsync();


    if (Role == null)
    {
        return Content("無效參數!");
    }

    return Page();
}

public async Task<IActionResult> OnPostRoleEdit_btnSaveClose_ClickAsync()
{
    if (ModelState.IsValid)
    {
        DB.Entry(Role).State = EntityState.Modified;
        await DB.SaveChangesAsync();

        // 關閉本窗體(觸發窗體的關閉事件)
        ActiveWindow.HidePostBack();
    }

    return UIHelper.Result();
}

這里 async Task 表示一個異步函數,在 EFCore查詢中,通過 await 關鍵字表明一個異步調用。

這段代碼的同步形式:

[BindProperty]
public Role Role { get; set; }

public IActionResult OnGet(int id)
{
    Role = DB.Roles
        .Where(m => m.ID == id).AsNoTracking().FirstOrDefault();


    if (Role == null)
    {
        return Content("無效參數!");
    }

    return Page();
}

public IActionResult OnPostRoleEdit_btnSaveClose_Click()
{
    if (ModelState.IsValid)
    {
        DB.Entry(Role).State = EntityState.Modified;
        DB.SaveChanges();

        // 關閉本窗體(觸發窗體的關閉事件)
        ActiveWindow.HidePostBack();
    }

    return UIHelper.Result();
}

除了 async Task await 等幾個關鍵詞,以及函數名的Async 后綴之外,其他地方和異步代碼一模一樣。

是不是很簡單,C#提供了如此優雅的代碼來實現異步編程,讓新手簡單看一眼就明白了,這也沒誰了。

 

2.3 Authorize特性和自定義權限驗證過濾器

2.3.1 Authorize登錄授權

登錄之后,我們把 [Authorize] 特性添加到 BaseAdminModel 基類上,這樣所有的 /Admin 目錄下的頁面都受到了登錄保護。

每個 Pages/Admin/ 目錄中的頁面都繼承自 BaseAdminModel 類,比如角色編輯頁面:

public class DeptEditModel : BaseAdminModel

當然這里僅僅是登錄授權保護!

 

 2.3.2 自定義CheckPower權限過濾器

那么該如何判斷登錄用戶是否有訪問某個頁面的權限呢?

我們自定義了一個權限驗證過濾器:

public class CheckPowerAttribute : ResultFilterAttribute
{
    /// <summary>
    /// 權限名稱
    /// </summary>
    public string Name { get; set; }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        HttpContext context = filterContext.HttpContext;
        
        if (!String.IsNullOrEmpty(Name) && !BaseModel.CheckPower(context, Name))
        {
            if (context.Request.Method == "GET")
            {
                BaseModel.CheckPowerFailWithPage(context);
                filterContext.Result = new EmptyResult();
            }
            else if (context.Request.Method == "POST")
            {
                BaseModel.CheckPowerFailWithAlert();
                filterContext.Result = UIHelper.Result();
            }
        }

    }
}

這個過濾器接受一個名為Name的字符串參數,用來表示一個權限名稱:

而一個用戶是否擁有這個權限,就看這個用戶所屬的角色是否擁有這個權限:

 

這個權限可以對頁面,以及頁面上的控件進行細粒度的控制。

2.3.3 CheckPower特性控制頁面的瀏覽權限

比如角色編輯頁面的瀏覽權限:

[CheckPower(Name = "CoreRoleEdit")]
public class RoleEditModel : BaseAdminModel
{
    // ....
}

 

2.3.4 表格行鏈接圖標的權限控制

既然不能編輯角色,那么在角色管理中,就應該禁用表格行中的編輯鏈接按鈕,如下圖所示:

 

這個怎么做到的呢?

首先,獲取當前用戶是否有編輯角色的權限:

public class RoleModel : BaseAdminModel
{
    public bool PowerCoreRoleEdit { get; set; }
        
    public async Task OnGetAsync()
    {
        PowerCoreRoleEdit = CheckPower("CoreRoleEdit");
        // ...
    }
    
    // ...
}

然后,在視圖標簽中:

<f:Grid ID="Grid1" ...>
    <Columns>
        <f:RenderField EnableColumnHide="false" EnableHeaderMenu="false" Width="50" RendererFunction="renderActionEdit"></f:RenderField>
        <f:RenderField EnableColumnHide="false" EnableHeaderMenu="false" Width="50" RendererFunction="renderActionDelete"></f:RenderField>
    </Columns>
</f:Grid>

注意,其中renderActionEdit 用來渲染編輯列,這是一個JS函數:

<script>

    var coreRoleEdit = @Convert.ToString(Model.PowerCoreRoleEdit).ToLower();

    function renderActionEdit(value, params) {
        var imageUrl = '@Url.Content("~/res/icon/pencil.png")';
        var disabledCls = coreRoleEdit ? '' : ' f-state-disabled';
        return '<a class="action-btn edit'+ disabledCls +'" href="javascript:;"><img class="f-grid-cell-icon" src="' + imageUrl + '"></a>';
    }
    
</script>

這就從UI上阻止用戶訪問角色編輯頁面,如果用戶一意孤行,想通過URL直接訪問,就會觸發自定義CheckPower過濾器:

2.3.5 表格行刪除按鈕的后台權限控制

上面表格的行刪除按鈕可以做類似的權限控制。但是實際的刪除操作是一個POST請求到頁面模型的處理器方法(Handler),而不是一個新的頁面(比如角色編輯頁面)。

既然用戶可以直接通過URL訪問角色編輯頁面,用戶通過可以偽造POST請求來執行刪除操作,這就需要對刪除的后台處理器方法進行保護!

public async Task<IActionResult> OnPostRole_DoPostBackAsync(...)
{
    if (actionType == "delete")
    {
        // 在操作之前進行權限檢查
        if (!CheckPower("CoreRoleDelete"))
        {
            CheckPowerFailWithAlert();
            return UIHelper.Result();
        }

        // ....
    }

    return UIHelper.Result();
}

上述的 CheckPower 方法是定義在基類中的一個公共方法:

public static bool CheckPower(HttpContext context, string powerName)
{
    // 當前登陸用戶的權限列表
    List<string> rolePowerNames = GetRolePowerNames(context);
    if (rolePowerNames.Contains(powerName))
    {
        return true;
    }

    return false;
}

 

新手往往忽略了這個保護操作,覺得頁面上不可點擊就萬事大吉,這是馬虎不得的。要記着這句話:客戶端的請求數據都是可以偽造的!

 

2.4 實體類模型定義的多對多聯接表

2.4.1 為什么 EF Core 不支持隱式聯接表

EF Core不支持沒有實體類來表示聯接表的多對多關系。 這一點剛開始讓人很是意外,畢竟都發展這么多年了,之前 EF 支持的東西 EF Core居然還不支持。

https://docs.microsoft.com/en-us/ef/core/modeling/relationships

Many-to-many relationships without an entity class to represent the join table are not yet supported. However, you can represent a many-to-many relationship by including an entity class for the join table and mapping two separate one-to-many relationships.

不過看到微軟這個文檔中描述的細節,我覺得微軟是不打算支持多對多關系的隱式聯接表了:

https://docs.microsoft.com/en-us/aspnet/core/data/ef-rp/complex-data-model?view=aspnetcore-3.1&tabs=visual-studio#many-to-many-relationships

Data models start out simple and grow. Join tables without payload (PJTs) frequently evolve to include payload. By starting with a descriptive entity name, the name doesn't need to change when the join table changes. Ideally, the join entity would have its own natural (possibly single word) name in the business domain.

簡單翻一下是這樣的:數據模型開始時很簡單,隨着內容的增加,純聯接表 (PJT) 通常會發展為有效負載的聯接表。

也就是微軟認為,隱式的聯接表隨着業務的增加很可能不適用,很可能會向聯接表中添加新的字段,這樣你還是需要創建顯式的聯接表。

 

既然如此!還不如不支持隱式的聯接表了。

 

好吧,看來 EF 中的隱式的聯接表是找不回來了。下面就來看下怎么在 EF Core 中使用顯式的聯接表吧。

 

2.4.2 定義聯接表模型類

聯接表模型定義很簡單,我們就以用戶角色關系表為例:

public class RoleUser
{
    public int RoleID { get; set; }
    public Role Role { get; set; }

    public int UserID { get; set; }
    public User User { get; set; }
    
}

 

在用戶表和角色表中,我們要分別添加導航屬性,來表示一對多的關系,在角色表中:

public class Role
{
    [Key]
    public int ID { get; set; }

    [Display(Name="名稱")]
    [StringLength(50)]
    [Required]
    public string Name { get; set; }

    [Display(Name = "備注")]
    [StringLength(500)]
    public string Remark { get; set; }


    public List<RoleUser> RoleUsers { get; set; }
}

注意,這里的導航屬性是 List<RoleUser> 。

 

作為對比,我們看下在 EF 版本中,這里的導航是:

public List<User> Users { get; set; }

 

這個區別很重要。也就是說,在EFCore中,用戶表的導航屬性是聯接表RoleUser集合,這將導致一系列的代碼更新,在隨后的一小節會有對比示例。

 

2.4.3 配置多對多關系

在EF版本中,可以方便的配置隱式聯接表的多對多關系,類似如下代碼:

modelBuilder.Entity<Role>()
    .HasMany(r => r.Users)
    .WithMany(u => u.Roles)
    .Map(x => x.ToTable("RoleUsers")
        .MapLeftKey("RoleID")
        .MapRightKey("UserID"));

 

而在 EF Core 版本中,實際上是不存在多對多的關系的,而是通過兩個一對多關系來表示,相應的代碼如下所示:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    // https://docs.microsoft.com/en-us/ef/core/modeling/relationships
    modelBuilder.Entity<RoleUser>()
        .ToTable("RoleUsers")
        .HasKey(t => new { t.RoleID, t.UserID });
    modelBuilder.Entity<RoleUser>()
        .HasOne(u => u.User)
        .WithMany(u => u.RoleUsers)
        .HasForeignKey(u => u.UserID);
    modelBuilder.Entity<RoleUser>()
       .HasOne(u => u.Role)
       .WithMany(u => u.RoleUsers)
       .HasForeignKey(u => u.RoleID);
       
}

代碼稍顯復雜,但結構還是比較清晰的:

  • 定義聯接表名為RoleUsers,並指定組合鍵RoleID和UserID
  • HasOne + WithMany 組合,來定義一對多關系
  • 使用兩個一對多關系,來迂回表示多對多關系

 

2.4.4 聯接表相關代碼更新

由於實體聯接表的引入,我們需要對多處代碼進行重構。下面給出幾個示例。

 

1. BaseModel中的GetRolePowerNames方法,之前 EF 版代碼:

db.Roles.Include(r => r.Powers).Where(r => roleIDs.Contains(r.ID)).ToList();

更新為 EF Core 版代碼:

db.Roles.Include(r => r.RolePowers).ThenInclude(rp => rp.Power).Where(r => roleIDs.Contains(r.ID)).ToList();

 

2. 用戶編輯頁面初始化代碼,之前 EF 版代碼:

DB.Users.Include(u => u.Roles).Where(m => m.ID == id).FirstOrDefault();

//...

String.Join(",", CurrentUser.Roles.Select(r => r.Name).ToArray());

更新為 EF Core 版代碼:

await DB.Users.Include(u => u.RoleUsers).ThenInclude(ru => ru.Role).Where(m => m.ID == id).FirstOrDefaultAsync();

// ...

String.Join(",", CurrentUser.RoleUsers.Select(ru => ru.Role.Name).ToArray());

 

3. 職稱列表頁面刪除行代碼,之前 EF 版代碼:

DB.Users.Where(u => u.Titles.Any(r => r.ID == deletedRowID)).Count();

更新為 EF Core 版代碼:

await DB.Users.Where(u => u.TitleUsers.Any(r => r.TitleID == deletedRowID)).CountAsync();

 

2.4.5 新增 IKey2ID 接口

在用戶編輯頁面,我們需要對用戶所屬的角色進行整體替換,類似的處理還有很多,我們把類似的操作都列出來:

  • 替換用戶所屬的角色列表
  • 替換用戶所屬的職稱列表
  • 替換角色的權限列表
  • 向角色中添加用戶列表
  • 向職稱中添加用戶列表
  • 新增用戶時,添加角色列表
  • 新增用戶時,添加職稱列表

其實所有這些操作都是對多對多聯接表的操作,為了避免在多處出現類似的重復代碼,我們新增了一個 IKey2ID 接口,表示有組合主鍵的聯接表:

public interface IKey2ID
{
    int ID1 { get; set; }

    int ID2 { get; set; }

}

用戶角色聯接表是實現這個接口的:

public class RoleUser : IKey2ID
{
    public int RoleID { get; set; }
    public Role Role { get; set; }

    public int UserID { get; set; }
    public User User { get; set; }


    [NotMapped]
    public int ID1
    {
        get
        {
            return RoleID;
        }
        set
        {
            RoleID = value;
        }
    }
    [NotMapped]
    public int ID2
    {
        get
        {
            return UserID;
        }
        set
        {
            UserID = value;
        }
    }

}

看似簡單的代碼,卻蘊藏着我們的深入思考。為了在后期代碼中用到大量的 Lambda 表達式,我們就需要固定的屬性名 ID1 和 ID2。

我們使用命名約定,將兩個主鍵分別映射到 ID1 和 ID2,在不同的聯接表中,含義是不同的:

  • RoleUser:ID1 => RoleID, ID2 => UserID
  • TitleUser:ID1 => TitleID, ID2 => UserID
  • RolePower:ID1 => RoleID, ID2 => PowerID

在基類(BaseModel)中,新增對聯接表的公共操作:

protected T Attach2<T>(int keyID1, int keyID2) where T : class, IKey2ID, new()
{
    T t = DB.Set<T>().Local.Where(x => x.ID1 == keyID1 && x.ID2 == keyID2).FirstOrDefault();
    if (t == null)
    {
        t = new T { ID1 = keyID1, ID2 = keyID2 };
        DB.Set<T>().Attach(t);
    }
    return t;
}

protected void AddEntities2<T>(int keyID1, int[] keyID2s) where T : class, IKey2ID, new()
{
    foreach (int id in keyID2s)
    {
        T t = Attach2<T>(keyID1, id);
        DB.Entry(t).State = EntityState.Added;
    }
}

protected void AddEntities2<T>(int[] keyID1s, int keyID2) where T : class, IKey2ID, new()
{
    foreach (int id in keyID1s)
    {
        T t = Attach2<T>(id, keyID2);
        DB.Entry(t).State = EntityState.Added;
    }
}

protected void RemoveEntities2<T>(List<T> existEntities, int[] keyID1s, int[] keyID2s) where T : class, IKey2ID, new()
{
    List<T> itemsTobeRemoved;
    if (keyID1s == null)
    {
        itemsTobeRemoved = existEntities.Where(x => keyID2s.Contains(x.ID2)).ToList();
    }
    else
    {
        itemsTobeRemoved = existEntities.Where(x => keyID1s.Contains(x.ID1)).ToList();
    }
    itemsTobeRemoved.ForEach(e => existEntities.Remove(e));
}

protected void ReplaceEntities2<T>(List<T> existEntities, int keyID1, int[] keyID2s) where T : class, IKey2ID, new()
{
    if (keyID2s.Length == 0)
    {
        existEntities.Clear();
    }
    else
    {
        int[] tobeAdded = keyID2s.Except(existEntities.Select(x => x.ID2)).ToArray();
        int[] tobeRemoved = existEntities.Select(x => x.ID2).Except(keyID2s).ToArray();

        AddEntities2<T>(keyID1, tobeAdded);
        RemoveEntities2<T>(existEntities, null, tobeRemoved);
    }
}

protected void ReplaceEntities2<T>(List<T> existEntities, int[] keyID1s, int keyID2) where T : class, IKey2ID, new()
{
    if (keyID1s.Length == 0)
    {
        existEntities.Clear();
    }
    else
    {
        int[] tobeAdded = keyID1s.Except(existEntities.Select(x => x.ID1)).ToArray();
        int[] tobeRemoved = existEntities.Select(x => x.ID1).Except(keyID1s).ToArray();

        AddEntities2<T>(tobeAdded, keyID2);
        RemoveEntities2<T>(existEntities, tobeRemoved, null);
    }
}

這里的 AddEntities2 和 ReplaceEntities2 分別有兩個重載實現,對應於 ID1 和 ID2 兩個互換的不同情況。

 

這里的實現其實非常巧妙,從優雅的調用就能看的出來,舉例如下:

  • 替換角色的權限列表
ReplaceEntities2<RolePower>(role.RolePowers, selectedRoleID, selectedPowerIDs);
  • 向角色中添加用戶列表
AddEntities2<RoleUser>(roleID, selectedRowIDs);

 

2.5 表單和表格的快速模型初始化

 FineUICore的表單和表格控件都支持快速模型初始化綁定,通過一個簡單的 For 屬性,讓我們少些很多代碼,下面通過 FineUICore 官網示例做個簡單的對比。

2.5.1 表單控件的快速模型初始化

手工設置表單字段屬性的示例:

<f:SimpleForm ShowHeader="false" BodyPadding="10" ShowBorder="false" ID="SimpleForm1">
    <Items>
        <f:TextBox ShowRedStar="true" Required="true" Label="用戶名" MaxLength="20" ID="UserName"></f:TextBox>
        <f:TextBox ShowRedStar="true" Required="true" TextMode="Password" RequiredMessage="密碼不能為空!" EnableValidateTrim="false" Label="密碼" ID="Password"
                   MaxLength="9" MaxLengthMessage="密碼最大為 9 個字符!" MinLength="3" MinLengthMessage="密碼最小為 3 個字符!" Regex="^(?:[0-9]+[a-zA-Z]|[a-zA-Z]+[0-9])[a-zA-Z0-9]*$" RegexMessage="密碼至少包含一個字母和數字!"></f:TextBox>
    </Items>
</f:SimpleForm>

代碼來自:https://pages.fineui.com/#/DataModel/Login

 

For屬性快速設置的示例:

<f:SimpleForm ShowHeader="false" BodyPadding="10" ShowBorder="false" ID="SimpleForm1">
    <Items>
        <f:TextBox For="CurrentUser.UserName"></f:TextBox>
        <f:TextBox For="CurrentUser.Password"></f:TextBox>
    </Items>
</f:SimpleForm>

代碼來自:https://pages.fineui.com/#/DataModel/LoginModel

 

這兩個代碼實現的功能是一模一樣的,只不過 For 屬性會從模型類中讀取字段的注解值,並自動設置相應的屬性:

public class User
{
    [Required]
    [Display(Name = "用戶名")]
    [StringLength(20)]
    public string UserName { get; set; }
    

    [Required(ErrorMessage = "用戶密碼不能為空!", AllowEmptyStrings = true)]
    [Display(Name = "密碼")]
    [MaxLength(9, ErrorMessage = "密碼最大為 9 個字符!")]
    [MinLength(3, ErrorMessage = "密碼最小為 3 個字符!")]
    [DataType(DataType.Password)]
    [RegularExpression("^(?:[0-9]+[a-zA-Z]|[a-zA-Z]+[0-9])[a-zA-Z0-9]*$", ErrorMessage = "密碼至少包含一個字母和數字!")]
    public string Password { get; set; }

}

 

這樣做有兩個明顯的好處:

  • 簡化代碼
  • 去除重復,減少人為的輸入錯誤,以及后期更新時可能存在不一致

 

頁面顯示效果:

 

AppBoxCore 中的所有表單都應用了 For 屬性快速設置,因此頁面代碼非常簡潔,看一下相對比較簡單的角色編輯頁面:

<f:Panel ID="Panel1" ShowBorder="false" ShowHeader="false" AutoScroll="true" IsViewPort="true" Layout="VBox">
<Toolbars>
    <f:Toolbar ID="Toolbar1">
        <Items>
            <f:Button ID="btnClose" Icon="SystemClose" Text="關閉">
                <Listeners>
                    <f:Listener Event="click" Handler="F.activeWindow.hide();"></f:Listener>
                </Listeners>
            </f:Button>
            <f:ToolbarSeparator></f:ToolbarSeparator>
            <f:Button ID="btnSaveClose" ValidateForms="SimpleForm1" Icon="SystemSaveClose" OnClick="@Url.Handler("RoleEdit_btnSaveClose_Click")" OnClickFields="SimpleForm1" Text="保存后關閉"></f:Button>
        </Items>
    </f:Toolbar>
</Toolbars>
<Items>
    <f:SimpleForm ID="SimpleForm1" ShowBorder="false" ShowHeader="false" BodyPadding="10">
        <Items>
            <f:HiddenField For="Role.ID"></f:HiddenField>
            <f:TextBox For="Role.Name">
            </f:TextBox>
            <f:TextArea For="Role.Remark"></f:TextArea>
        </Items>
    </f:SimpleForm>
</Items>
</f:Panel>

 

2.5.2 表格控件的快速模型初始化

手工設置表格列屬性的示例:

<f:Grid ID="Grid1" IsFluid="true" CssClass="blockpanel" ShowBorder="true" ShowHeader="true" Title="表格" DataIDField="Id" DataTextField="Name" DataSource="@DataSourceUtil.GetDataTable()">
    <Columns>
        <f:RowNumberField />
        <f:RenderField HeaderText="姓名" DataField="Name" Width="100" />
        <f:RenderField HeaderText="性別" DataField="Gender" FieldType="Int" RendererFunction="renderGender" Width="80" />
        <f:RenderField HeaderText="入學年份" DataField="EntranceYear" FieldType="Int" Width="100" />
        <f:RenderCheckField HeaderText="是否在校" DataField="AtSchool" RenderAsStaticField="true" Width="100" />
        <f:RenderField HeaderText="所學專業" DataField="Major" RendererFunction="renderMajor" ExpandUnusedSpace="true" MinWidth="150" />
        <f:RenderField HeaderText="分組" DataField="Group" RendererFunction="renderGroup" Width="80" />
        <f:RenderField HeaderText="注冊日期" DataField="LogTime" FieldType="Date" Renderer="Date" RendererArgument="yyyy-MM-dd" Width="100" />
    </Columns>
</f:Grid>

代碼來自:https://pages.fineui.com/#/Grid/Grid

 

For屬性快速設置的示例:

<f:Grid ID="Grid1" IsFluid="true" CssClass="blockpanel" ShowBorder="true" ShowHeader="true" Title="表格" DataIDField="Id" DataTextField="Name" DataSource="@Model.Students">
        <Columns>
            <f:RowNumberField />
            <f:RenderField For="Students.First().Name" />
            <f:RenderField For="Students.First().Gender" RendererFunction="renderGender" Width="80" />
            <f:RenderField For="Students.First().EntranceYear" />
            <f:RenderCheckField For="Students.First().AtSchool" RenderAsStaticField="true" />
            <f:RenderField For="Students.First().Major" RendererFunction="renderMajor" ExpandUnusedSpace="true" MinWidth="150" />
            <f:RenderField For="Students.First().Group" RendererFunction="renderGroup" Width="80" />
            <f:RenderField For="Students.First().EntranceDate" />
        </Columns>
    </f:Grid>

代碼來自:https://pages.fineui.com/#/DataModel/Grid

 

同樣,這兩個代碼實現的功能是一模一樣的,只不過 For 屬性會從模型類中讀取字段的注解值,並自動設置相應的屬性:

public class Student
{
    [Key]
    public int Id { get; set; }

    [Required]
    [Display(Name = "姓名")]
    [StringLength(20)]
    public string Name { get; set; }

    [Required]
    [Display(Name = "性別")]
    public int Gender { get; set; }

    [Required]
    [Display(Name = "入學年份")]
    public int EntranceYear { get; set; }

    [Required]
    [Display(Name = "是否在校")]
    public bool AtSchool { get; set; }

    [Required]
    [Display(Name = "所學專業")]
    [StringLength(200)]
    public string Major { get; set; }

    [Required]
    [Display(Name = "分組")]
    public int Group { get; set; }


    [Display(Name = "注冊日期")]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")]
    public DateTime? EntranceDate { get; set; }

}

 

頁面顯示效果:

 

同樣,AppBoxCore中所有的表格都使用了快速模型初始化。

 

2.6 對比 Dapper 和 EFCore 的實現細節 

除了 AppBoxCore 項目使用 EF Core 之外,我們還有另一個實現相同功能的項目:AppBoxCore.Dapper ,兩者的區別就在於訪問數據庫的方式:

  • AppBoxCore項目的技術架構:FineUICore + Razor Pages + Entity Framework Core
  • AppBoxCore項目的技術架構:FineUICore + Razor Pages + Dapper

下面會對幾處代碼在 EF Core 和 Dapper 下的不同進行對比。

 

2.6.1 角色列表頁面

Dapper版:

private async Task<IEnumerable<Role>> Role_GetDataAsync(PagingInfoViewModel pagingInfo, string ttbSearchMessage)
{
    var builder = new WhereBuilder();

    string searchText = ttbSearchMessage?.Trim();
    if (!String.IsNullOrEmpty(searchText))
    {
        builder.AddWhere("roles.Name like @SearchText");
        builder.AddParameter("SearchText", "%" + searchText + "%");
    }

    // 獲取總記錄數(在添加條件之后,排序和分頁之前)
    pagingInfo.RecordCount = await CountAsync<Role>(builder);

    // 排列和數據庫分頁
    return await SortAndPageAsync<Role>(builder, pagingInfo);
}

 

EF Core版:

private async Task<IEnumerable<Role>> Role_GetDataAsync(PagingInfoViewModel pagingInfo, string ttbSearchMessage)
{
    IQueryable<Role> q = DB.Roles;

    string searchText = ttbSearchMessage?.Trim();
    if (!String.IsNullOrEmpty(searchText))
    {
        q = q.Where(p => p.Name.Contains(searchText));
    }

    // 獲取總記錄數(在添加條件之后,排序和分頁之前)
    pagingInfo.RecordCount = await q.CountAsync();

    // 排列和數據庫分頁
    q = SortAndPage<Role>(q, pagingInfo);

    return await q.ToListAsync();
}

 

2.6.2 向角色中添加用戶列表

Dapper版:

public async Task<IActionResult> OnPostRoleUserNew_btnSaveClose_ClickAsync(int roleID, int[] selectedRowIDs)
{
    await DB.ExecuteAsync("insert roleusers (UserID, RoleID) values (@UserID, @RoleID)", selectedRowIDs.Select(u => new { UserID = u, RoleID = roleID }).ToList());

    // 關閉本窗體(觸發窗體的關閉事件)
    ActiveWindow.HidePostBack();

    return UIHelper.Result();
}

 

EF Core版:

public async Task<IActionResult> OnPostRoleUserNew_btnSaveClose_ClickAsync(int roleID, int[] selectedRowIDs)
{
    AddEntities2<RoleUser>(roleID, selectedRowIDs);
    await DB.SaveChangesAsync();

    // 關閉本窗體(觸發窗體的關閉事件)
    ActiveWindow.HidePostBack();

    return UIHelper.Result();
}

 

2.6.3 編輯用戶

Dapper版:

var _user = await GetUserByIDAsync(CurrentUser.ID);

_user.ChineseName = CurrentUser.ChineseName;
_user.Gender = CurrentUser.Gender;
_user.Enabled = CurrentUser.Enabled;
_user.Email = CurrentUser.Email;
_user.CompanyEmail = CurrentUser.CompanyEmail;
_user.OfficePhone = CurrentUser.OfficePhone;
_user.OfficePhoneExt = CurrentUser.OfficePhoneExt;
_user.HomePhone = CurrentUser.HomePhone;
_user.CellPhone = CurrentUser.CellPhone;
_user.Remark = CurrentUser.Remark;

if (String.IsNullOrEmpty(hfSelectedDept))
{
    _user.DeptID = null;
}
else
{
    _user.DeptID = Convert.ToInt32(hfSelectedDept);
}

using (var transactionScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
    // 更新用戶
    await ExecuteUpdateAsync<User>(DB, _user);

    // 更新用戶所屬角色
    int[] roleIDs = StringUtil.GetIntArrayFromString(hfSelectedRole);
    await DB.ExecuteAsync("delete from roleusers where UserID = @UserID", new { UserID = _user.ID });
    await DB.ExecuteAsync("insert roleusers (UserID, RoleID) values (@UserID, @RoleID)", roleIDs.Select(u => new { UserID = _user.ID, RoleID = u }).ToList());

    // 更新用戶所屬職務
    int[] titleIDs = StringUtil.GetIntArrayFromString(hfSelectedTitle);
    await DB.ExecuteAsync("delete from titleusers where UserID = @UserID", new { UserID = _user.ID });
    await DB.ExecuteAsync("insert titleusers (UserID, TitleID) values (@UserID, @TitleID)", titleIDs.Select(u => new { UserID = _user.ID, TitleID = u }).ToList());


    transactionScope.Complete();
}

注意:由於涉及多個表保存,所以Dapper版借助事務來完成多個表的數據更新操作。

 

EF Core版:

var _user = await DB.Users
    .Include(u => u.Dept)
    .Include(u => u.RoleUsers)
    .Include(u => u.TitleUsers)
    .Where(m => m.ID == CurrentUser.ID).FirstOrDefaultAsync();


_user.ChineseName = CurrentUser.ChineseName;
_user.Gender = CurrentUser.Gender;
_user.Enabled = CurrentUser.Enabled;
_user.Email = CurrentUser.Email;
_user.CompanyEmail = CurrentUser.CompanyEmail;
_user.OfficePhone = CurrentUser.OfficePhone;
_user.OfficePhoneExt = CurrentUser.OfficePhoneExt;
_user.HomePhone = CurrentUser.HomePhone;
_user.CellPhone = CurrentUser.CellPhone;
_user.Remark = CurrentUser.Remark;


int[] roleIDs = StringUtil.GetIntArrayFromString(hfSelectedRole);
ReplaceEntities2<RoleUser>(_user.RoleUsers, roleIDs, _user.ID);

int[] titleIDs = StringUtil.GetIntArrayFromString(hfSelectedTitle);
ReplaceEntities2<TitleUser>(_user.TitleUsers, titleIDs, _user.ID);

if (String.IsNullOrEmpty(hfSelectedDept))
{
    _user.DeptID = null;
}
else
{
    _user.DeptID = Convert.ToInt32(hfSelectedDept);
}

await DB.SaveChangesAsync();

 

2.6.4 Menu模型類的ViewPowerName屬性

在 Dapper 版,Menu的模型類中有 ViewPowerName 屬性:

public class Menu : ICustomTree, IKeyID, ICloneable
{
    [Key]
    public int ID { get; set; }

    [Display(Name = "菜單名稱")]
    [StringLength(50)]
    [Required]
    public string Name { get; set; }

    // ...

    [Display(Name = "上級菜單")]
    public int? ParentID { get; set; }

    [Display(Name = "瀏覽權限")]
    public int? ViewPowerID { get; set; }


    [NotMapped]
    [Display(Name = "瀏覽權限")]
    public string ViewPowerName { get; set; }
    
}

 

而 EF Core版中,Menu模型類中沒有 ViewPowerName 屬性,但是存在 ViewPower 導航屬性:

public class Menu : ICustomTree, IKeyID, ICloneable
{
    [Key]
    public int ID { get; set; }

    [Display(Name = "菜單名稱")]
    [StringLength(50)]
    [Required]
    public string Name { get; set; }

    // ...

    [Display(Name = "上級菜單")]
    public int? ParentID { get; set; }
    public Menu Parent { get; set; }


    [Display(Name = "瀏覽權限")]
    public int? ViewPowerID { get; set; }
    public Power ViewPower { get; set; }
    
}

 

這個差異導致了多處代碼不盡相同,不過總的來說還算清晰,我們一一列舉出來供大家參考。

1. 編輯頁面(獲取初始數據)
EFCore版:

public async Task<IActionResult> OnGetAsync(int id)
{
    Menu = await DB.Menus
        .Include(m => m.Parent)
        .Include(m => m.ViewPower)
        .Where(m => m.ID == id).FirstOrDefaultAsync();

    if (Menu == null)
    {
        return Content("無效參數!");
    }

    MenuEdit_LoadData(id);

    return Page();
}

 

Dapper版:

public async Task<IActionResult> OnGetAsync(int id)
{
    Menu = await DB.QuerySingleOrDefaultAsync<Models.Menu>("select menus.*, powers.Name ViewPowerName from menus left join powers on menus.ViewPowerID = powers.ID where menus.ID = @MenuID", new { MenuID = id });
    
    if (Menu == null)
    {
        return Content("無效參數!");
    }

    MenuEdit_LoadData(id);

    return Page();
}

 

2. 列表頁面
EFCore版:

<f:RenderField For="Menus.First().ViewPower.Name"></f:RenderField>

Dapper版:

<f:RenderField For="Menus.First().ViewPowerName"></f:RenderField>


3. 編輯頁面
EFCore版:

<f:TextBox For="Menu.ViewPowerID" Text="@(Model.Menu.ViewPower == null ? "" : Model.Menu.ViewPower.Name)" Name="ViewPowerName"></f:TextBox>


Dapper版:

<f:TextBox For="Menu.ViewPowerName"></f:TextBox>

 

4. 編輯頁面后台
EFCore版:

OnPostMenuEdit_btnSaveClose_ClickAsync(string ViewPowerName)

 

Dapper版:

OnPostMenuEdit_btnSaveClose_ClickAsync()

 在代碼中,可以通過 Menu.ViewPowerName 獲取用戶的輸入值。

 

 

三、截圖賞析

FineUICore 內置了幾十個主題,這里就分別選取一個深色主題和淺色主題以饗讀者。

 

3.1 深色主題(Dark Hive) 

  

 

3.2 淺色主題(Pure Purple)

 

 

 

四、源代碼下載

FineUICore(基礎版)非免費軟件,你可以加入【三石和他的朋友們】知識星球下載 AppBoxCore 的完整項目源代碼:

https://fineui.com/fans/

FineUICore算是國內堅持在 ASP.NET Core 陣營僅有的控件庫了,前后歷經 12 年的時間持續不斷的更新,細節上追求精益求精,期待你的加入。

我們來回顧下從 FineUIPro 到 FineUIMvc,再到 FineUICore 關鍵時間點:

  • v1.0.0 於 2014-07-30 發布,這也是我們 FineUIPro 產品線的第一個版本,實現了開源版(100多個版本)的全部功能。
  • v2.0.0 於 2014-12-10 發布,半年的時間內我們快速迭代了 10 個小版本,並發布功能完善的 2.0 大版本。
  • v3.0.0 於 2016-03-16 發布,在此期間我們不僅支持大數據表格,而且對手機、平板、桌面進行了全適配。
  • v4.0.0 於 2017-10-30 發布,期間我們上線了新產品FineUIMvc 和純前端庫F.js,並且支持了CSS3動畫。
  • v5.0.0 於 2018-04-23 發布,支持ASP.NET Core的全新產品FineUICore來了,並且創新了基於像素的響應式布局。
  • v6.0.0 於 2019-09-20 發布,方便將WebForms快速遷移到FineUICore,並帶來一系列的功能和性能改善。
  • v6.2.0 於 2020-02-08 發布,將 FineUICore 升級到最新的 .Net Core 3.1。

 

AppBoxCore v6.2 更新記錄:

+2020-03-31 v6.2
	-升級到 FineUICore(基礎版)v6.2.0。
	-基於 ASP.NET Core 的 RazorPages 和 TagHelpers 技術架構。
	-使用 EntityFramework Core 訪問數據庫。
	-基於 .Net Core 3.1。
	-部分代碼參考網友【時不我待】的實現:https://t.zsxq.com/UBAqN3N
	+功能更新。
		+頁面處理器(GET/POST)和數據庫操作全部改為異步調用(async/await)。
			-服務器的可用線程是有限的,在高負載情況下的可能所有線程都被占用,此時服務器就無法處理新的請求,直到有線程被釋放。 
			-使用同步代碼時,可能會出現多個線程被占用而不能執行任何操作的情況,因為它們正在等待 I/O 完成。 
			-使用異步代碼時,當線程正在等待 I/O 完成時,服務器可以將其線程釋放用於處理其他請求。
		-將基類的ExecuteUpdate、Sort、SortAndPage、Count、FindByID方法全部改為異步調用。
		-增加頁面模型基類BaseAdminModel,並設置[Authorize]特性以阻止未登陸用戶訪問管理頁面。
		-頁面模型類中,將對ViewBag的調用改為類屬性。
		+EFCore不支持沒有實體類來表示聯接表的多對多關系。
			-無有效負載的多對多聯接表有時稱為純聯接表 (PureJoinTable)。
			-數據模型開始時很簡單,隨着內容的增加,純聯接表 (PJT) 通常會發展為有效負載的聯接表。
			-新增實體類:RolePower、RoleUser、TitleUser。
			-更新AppBoxCoreContext中的OnModelCreating,包含多對多,一對多,單個導航等定義。
			-更新模型類User,刪除Roles和Titles導航屬性,新增RoleUsers和TitleUsers導航屬性。
		-更新AppBoxCoreDatabaseInitializer,並在程序啟用階段調用(Program.cs)。
		+使用依賴注入添加數據庫連接實例。
			-在Startup.cs的ConfigureServices中,通過AddDbContext來注冊EFCore服務。
			+在BaseModel.cs類中獲取數據庫連接實例。
				-FineUICore.PageContext.GetRequestService<AppBoxCoreContext>();
				-由於同時需要在靜態函數和實例函數中調用,所以通過當前請求上下文獲取服務對象。
				-如果僅需要在實例函數中調用,可以通過類的構造函數注入。
		+在頁面初始化查詢中,添加對AsNoTracking()的調用。
			-如果返回的實體未在當前上下文中更新(未調用SaveChanges),AsNoTracking方法將會提升性能。
		+由於現在需要用關聯表表示多對多關系,所以需要對之前的代碼進行重構。
			+重構BaseModel中的GetRolePowerNames方法。
				-db.Roles.Include(r => r.Powers).Where(r => roleIDs.Contains(r.ID)).ToList();
				-改為:
				-db.Roles.Include(r => r.RolePowers).ThenInclude(rp => rp.Power).Where(r => roleIDs.Contains(r.ID)).ToList();
			+重構用戶編輯頁面初始化代碼。
				-DB.Users.Include(u => u.Roles).Where(m => m.ID == id).FirstOrDefault();
				-String.Join(",", CurrentUser.Roles.Select(r => r.Name).ToArray());
				-改為:
				-await DB.Users.Include(u => u.RoleUsers).ThenInclude(ru => ru.Role).Where(m => m.ID == id).FirstOrDefaultAsync();
				-String.Join(",", CurrentUser.RoleUsers.Select(ru => ru.Role.Name).ToArray());
			+重構職稱列表頁面刪除行代碼。
				-DB.Users.Where(u => u.Titles.Any(r => r.ID == deletedRowID)).Count();
				-改為:
				-await DB.Users.Where(u => u.TitleUsers.Any(r => r.TitleID == deletedRowID)).CountAsync();
		+更新用戶密碼頁面,設置HiddenField的Name=hfUserID屬性,以便在后台通過函數參數獲取值。
			-可選實現:設置CurrentUser的[BindProperty]特性,然后通過CurrentUser.ID獲取。
			-可選實現:函數參數IFormCollection values,然后通過Convert.ToInt32(values["CurrentUser.ID"].ToString())獲取。
		-使用TextBox標簽的For屬性(For=Title.Name)時,無需設置Required=true和ShowRedStar=true,這兩個屬性會從Title模型的特性中讀取並設置。
		-用戶列表頁面的觸發器輸入框,由於設置了OnTrigger2ClickFields=Panel1,因此無需額外傳入參數new Parameter("ttbSearchMessage","F.ui.ttbSearchMessage.getValue()")。
		-菜單編輯頁面,如果指定的瀏覽權限名稱錯誤,則彈出框提示(瀏覽權限 XXX 不存在!)。
		+新增IKey2ID接口。
			-RolePower、RoleUser、TitleUser實現了IKey2ID接口,BaseModel新增AddEntities2、ReplaceEntities2方法。
			+編輯用戶頁面。
				-ReplaceEntities<Role>(_user.Roles, roleIDs); 改為:ReplaceEntities2<RoleUser>(_user.RoleUsers, roleIDs, _user.ID);
				-_user.Dept = Attach<Dept>(Convert.ToInt32(hfSelectedDept)); 改為:_user.DeptID = Convert.ToInt32(hfSelectedDept); 
			+新增用戶頁面。
				-AddEntities<Role>(user.Roles, roleIDs); 改為:AddEntities2<RoleUser>(roleIDs, CurrentUser.ID);
		+Menu模型類,Dapper版有ViewPowerName屬性,而EFCore版沒有ViewPowerName屬性。
			+列表頁面:
				-EFCore版:<f:RenderField For="Menus.First().ViewPower.Name"></f:RenderField>
				-Dapper版:<f:RenderField For="Menus.First().ViewPowerName"></f:RenderField>
			+編輯頁面:
				-EFCore版:<f:TextBox For="Menu.ViewPowerID" Text="@(Model.Menu.ViewPower == null ? "" : Model.Menu.ViewPower.Name)" Name="ViewPowerName"></f:TextBox>
				-Dapper版:<f:TextBox For="Menu.ViewPowerName"></f:TextBox>
			+編輯頁面后台:
				-EFCore版:OnPostMenuEdit_btnSaveClose_ClickAsync(string ViewPowerName)
				-Dapper版:OnPostMenuEdit_btnSaveClose_ClickAsync(),通過 Menu.ViewPowerName 獲取用戶的輸入值。

  

 

今天恰逢【壯族三月三】(廣西法定節假日),家家戶戶都有做五色糯米飯的傳統,人們采來紅藍草、黃飯花、楓葉、紫蕃藤,用這些植物的汁浸泡糯米,做成紅、黃、黑、紫、白五色糯米飯。

其中以楓葉染成的黑色糯米飯最是香濃。

每個地方用的原料可能不大相同,比如這邊黃色糯米飯用的梔子染色的。

紫蕃藤又稱紫藍草,和紅藍草是同一個品種。紫藍草的葉片稍長,顏色稍深,煮出來的就是紫色,而紅藍草的葉片較圓,顏色較淺,煮出來的就是紅色。

這也正應了那句俗話【紅的發紫】,看來紅色和紫色本是一家。

 

 

AppBoxCore(Dapper版)現已發布! 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM