Scaffolding 雖然可以產生清單及新增修改刪除介面,但公版介面與實際需求總有些差距,以本次的情境為例,有幾個地方需要修改:

  1. 清單項目應依日期順序顯示,預設只顯示當月,必要時則可指定年月查詢歷史記錄
  2. 日期欄位雖為 DateTime,但輸入時只需提供日期就好,顯示時亦然
  3. 記錄限每日一筆,雖有 Unique Index 日期重複會報錯,但希望有更明確提示
  4. 狀態屬性有分 Status 及 StatusText,前者是列舉與後者為文字,我希望操作介面使用 Staus 並以下拉選單輸入
  5. 編輯更新時,日期欄位應為唯讀不允許修改

幾個簡單小調整,現在來看看該怎麼修改。

日期排序及查詢歷史記錄

清單資料查詢的程式碼在 Index.cshtml.cs,原本長這樣:

public async Task OnGetAsync()
{
    DailyRecord = await _context.Records.ToListAsync();
}

要修改查詢條件跟排序,傳統寫法是要修改 SQL 語法,使用 EF Core 時,我們只需對 _context.Records 加上 LINQ Where()、Order() 就好,EF Core 會自動幫忙對映成 SQL 語法,甚至不用管資料庫是 SQL、Oracle、MySQL 還是 SQLite,專心寫 LINQ 就好。由於要能指定年月,我為 OnGetAsync() 加上 year 跟 month 選擇性參數,預設為 null,參數未提供時用 DateTime.Today 取當月,傳回集合使用 LINQ Where() 限定月份區間,Sort() 依日期排序,如此網址 /Records 顯示當月資料,/Records?year=2019&month=12 可顯示 2019 12月記錄:

public async Task OnGetAsync(int? year = null, int? month = null)
{
    year = year ?? DateTime.Today.Year;
    //順便介紹C# 8.0 Compound Assignment寫法,跟上面效果相同但更簡潔
    month ??= DateTime.Today.Month;
    var startDate = new DateTime(year.Value, month.Value, 1);

    DailyRecord = await _context.Records
        .Where(o => o.Date >= startDate &&
                    o.Date < startDate.AddMonths(1))
        .OrderBy(o => o.Date)
        .ToListAsync();
}

Date 屬性輸入及顯示時只要日期不要時間

顯示部分需要改 DailyRecord.cs,在 Date 屬性加上 [DisplayFormat] Attribute,Index.cshtml、Details.cshtml、Delete.cshtml 有用到 @Html.DisplayFor(model => model.DailyRecord.Date),三處都將只顯示日期:

/// <summary>
/// 日期
/// </summary>
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")]
public DateTime Date { get; set; } = DateTime.Today;

至於輸入欄位,Create.cshtml 是用 asp-for Tag Helper,原本自動判斷使用日期加時間,input 加上 type="date" 強制指定型別就可以了:

<div class="form-group">
    <label asp-for="DailyRecord.Date" class="control-label"></label>
    <input type="date" asp-for="DailyRecord.Date" class="form-control" />
    <span asp-validation-for="DailyRecord.Date" class="text-danger"></span>
</div>

日期重複友善提示

原本輸入重複日期會直接噴錯:

我修改了 Create.cshtml.cs,在 OnPostAsync() 加入一段檢查,若資料庫已有當日資料,則在 Date 屬性標示當日資料已存在提示(技巧是利用 ModelState.AddModelError() 指定欄位名稱 "DailyRecord.Data" 加入自訂檢核錯誤訊息):

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    //檢查同日期是否已有資料,若是顯示日期重複
    if (_context.Records.Any(o => o.Date == DailyRecord.Date))
    {
        ModelState.AddModelError("DailyRecord.Date", "該日期記錄已存在");
        return Page();
    }

    _context.Records.Add(DailyRecord);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

顯示效果如下,是不是友善多了?

狀態改列舉下拉

修改處為 Create.cshtml 及 Edit.cshtml,原本為:

<div class="form-group">
    <label asp-for="DailyRecord.StatusText" class="control-label"></label>
    <input asp-for="DailyRecord.StatusText" class="form-control" />
    <span asp-validation-for="DailyRecord.StatusText" class="text-danger"></span>
</div>

欄位對象改為 select,並用 asp-items 搭配 Html.GetEnumSelectList<TEunm> 產生列舉選項:

<div class="form-group">
    <label asp-for="DailyRecord.Status" class="control-label"></label>
    <select asp-for="DailyRecord.Status"
            asp-items="Html.GetEnumSelectList<CRUDExample.Models.StatusFlags>()"
            class="form-control">
        <option selected value="">請選擇...</option>
    </select>
    <span asp-validation-for="DailyRecord.Status" class="text-danger"></span>
</div>

效果如下:

編輯更新時,日期不允許修改

修改位置在 Edit.cshtml.cs,在此我們順便觀摩 Razor Page Scaffolding 是如何更新一筆現有資料的。它使用 EF Attache() 將使用者傳入的 DailyRecord 物件聯結到資料庫現有,並強制修改其狀態為已修改(EntityState.Modified),如此在 SaveChanges() 時 EF Core 將執行 UPDATE Records SET ... WHERE ... 指令更新所有欄位。而我用了一個 DbConetxt.Entry(entityObject).Property(o => o.PropName).IsModified 的小技巧由於我要避開修改日期,故改為先由資料庫取得該筆資料,比對 Date 欄位是否有被使用者異動,若有則套用前面提過的 AddModelError() 技巧,在日期欄位提示不可修改。若日期未修改,則以正向表列方式更新欄位,SaveChanges() EFCore 將偵測屬性變化,只更新有修改的欄位:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    //由資料庫取得修改前版本
    var orig = _context.Records.SingleOrDefault(o => o.Id == DailyRecord.Id);
    if (orig == null) return NotFound();

    //檢查日期是否被更動,若是,拒絕更新
    if (orig.Date.CompareTo(DailyRecord.Date) != 0)
    {
        ModelState.AddModelError("DailyRecord.Date", "日期不可修改");
        return Page();
    }

    //以正向表列方式更新可更新欄位,SaveChanges()只會更新有異動的欄位
    orig.Status = DailyRecord.Status;
    orig.Remark = DailyRecord.Remark;
    orig.EventSummary = DailyRecord.EventSummary;
    orig.User = DailyRecord.User;

    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

效果如下:

另一種做法是修改 Edit.cshtml 將輸入欄位改為純文字。我偏好讓新增與編輯畫面一致些,決定這樣防呆就好。

小結

經過這番修改,CRUD 介面已到堪用程度,資料也已順利進入資料庫,用來匯出報表不成問題。希望這個展示有讓大家感受到「快速生出簡單網站自用」的暢快?ASP.NET Core 3.1 的成熟度已足,而開發輔助工具的貼心設計也符合微軟一向風格。當然,這種公版 UI 直接拿去正式專案肯定被打槍,但對還想快速上手的 ASP.NET Core 初心者來說,不失為學習與熟悉 ASP.NET Core Razor Page 的捷徑,快來加入 ASP.NET Core 的行列吧~

範例原始碼

我鼓勵大家自己用 Visual Studio 2019 照著文的步驟把網站做出來,但為了預防遇到疑難雜症時需要可執行樣本對照,我已把本系列文章的範例專案放上 Github (註:LocalDB 部分請依第二篇教學建立 .mdf 並修改連線字串才能跑),並依文章進度分成四次 Commit,有需要的同學請自取參考,祝大家 Coding 愉快。

Demostration of CRUD scaffolding Razor Pages customization.


Comments

# by

感謝黑大的系列文章~ 照著做,對core的熟悉度又增加了~

# by george

若未修改 Date,DbConetxt.Entry(entityObject).Property(o => o.PropName).IsModified 也會是 true?

# by Jeffrey

to george, 原寫法的確有問題(羞)。設定.State = EntityState.Modified 後所有屬性都會被標示為需更新。已修正程式寫法及內文,感謝你的指正。

# by Alan

照著此系列實作,又學了新的東西 感謝黑大的文章

# by 新手

請問我用tab頁籤怎麼預設畫面載進來

# by Jeffrey

to 新手,Tab頁籤是指前端框架元件?不同的元件寫法不同。

Post a comment