在網站開發的過程中,一開始一般會有下列幾個步驟:
- 拿到 designer 的 layout
- Developer 進行 view 的切版
- 定義 ViewModel 並接上假資料
這篇文章將針對一開始這幾個步驟,介紹如何透過 ASP.NET MVC 的 View, ViewModel, ChildAction 來進行開發。
前言
今年四月參加了 Skilltree 由 demo 所講授的 ASP.NET MVC5 實戰訓練營課程, 每一天課程之後都會有 homework 來讓學員練習,並且講師會針對學員各個 commit 進行 code review 跟給 comment (我的 TDD 課程也都是這樣進行,雖然很花講師時間,但我個人覺得這對學員是最有價值的一種方式)。因為 homework 設計相當實務且循序漸進,讓我獲益良多,所以打算針對 homework 需求,透過幾篇文章來紹上課以及自己上網查的技巧做一些簡介。
背景
當開發人員瞭解需求之後,通常開發過程如下:
- 先由 designer 設計出一版靜態 html 與 css 的版面
- 交由 developer 針對 View 需要什麼樣的邏輯、資料與呈現來進行切版套版
- Developer 依據 View 需要的資料意義,定義出 ViewModel ,並與後端 controller 進行互動,回傳假的 ViewModel 資料,確認 View 的呈現與需求單位期望一致
Homework 的需求是實作一個簡單的記事本功能,第一天的 homework 規範如下:
- 請使用「MoneyTemplate.html」作為樣版(就是你家設計提供的版型)
- 必須使用 Layout
- 下方列表必須有假資料(禁止寫死在HTML)可使用 ViewModel
- 上方表單部分如果有時間可以依據需求調整,如果沒時間可以將「MoneyTemplate.html」的部分直接複製過來。
靜態版面
Designer 所設計的 MoneyTemplate.html 版面,如下圖所示:
其 HTML 內容如下:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的記帳本</title>
<!--@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")-->
<link href="http://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand">我的記帳本</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="#">首頁</a>
<li><a href="#">關於</a>
<li><a href="#">連絡方式</a>
</ul>
</div>
</div>
</div>
<div class="container body-content">
<div class="well">
<form class="form-horizontal">
<div class="form-group">
<label for="category" class="col-sm-2 control-label">類別</label>
<div class="col-sm-10">
<select id="category" class="form-control">
<option value="" selected>請選擇</option>
<option>支出</option>
<option>收入</option>
</select>
</div>
</div>
<div class="form-group">
<label for="money" class="col-sm-2 control-label">金額</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="money" placeholder="金額">
</div>
</div>
<div class="form-group">
<label for="date" class="col-sm-2 control-label">日期</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="date" placeholder="日期">
</div>
</div>
<div class="form-group">
<label for="description" class="col-sm-2 control-label">備註</label>
<div class="col-sm-10">
<textarea class="form-control" id="description">
</textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-8 col-sm-4">
<button type="submit" class="btn btn-default">送出</button>
</div>
</div>
</form>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-bordered table-hover">
<tr>
<th>#</th>
<th>類別</th>
<th>日期</th>
<th>金額</th>
</tr>
<tr>
<td>1</td>
<td>支出</td>
<td>2016-01-01</td>
<td>300</td>
</tr>
<tr>
<td>2</td>
<td>支出</td>
<td>2016-01-02</td>
<td>1,600</td>
</tr>
<tr>
<td>3</td>
<td>支出</td>
<td>2016-01-03</td>
<td>8,00</td>
</tr>
</table>
</div>
</div>
<hr />
<footer>
<p>© 2016 - <a href="#">SkillTree</a></p>
</footer>
</div>
<!--@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")-->
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.1.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"></script>
</body>
</html>
定義 Layout 與 Controller
先建立一個 AccountingLayout 與 designer 所給的 HTML 一模一樣,接著將 <div class="container body-content">
中,<footer>
之前的內容切出來,由 @RenderBody()
取代。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的記帳本</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
<link href="http://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand">我的記帳本</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="#">首頁</a>
<li><a href="#">關於</a>
<li><a href="#">連絡方式</a>
</ul>
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2016 - <a href="#">SkillTree</a></p>
</footer>
</div>
@*@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")*@
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.1.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"></script>
</body>
</html>
依據記帳本的意義,定義出一個 AccountingController 如下:
public class AccountingController : Controller
{
// GET: Accounting
public ActionResult Index()
{
return View();
}
}
設計 Index.cshtml
在 Index()
中,點選 Add View ,自動產生 Index.cshtml 出來
選擇 Layout,產生 Index.cshtml
將 ViewBag.Title 的部分,改為「我的記帳本」,套用剛剛的 AccoutingLayout.cshtml,並將 Layout Title 的部分改為 ViewBag.Title。Index.cshtml 內容如下:
@{
ViewBag.Title = "我的記帳本";
Layout = "~/Views/Shared/_AccountingLayout.cshtml";
}
<div class="well">
<form class="form-horizontal">
<div class="form-group">
<label for="category" class="col-sm-2 control-label">類別</label>
<div class="col-sm-10">
<select id="category" class="form-control">
<option value="" selected>請選擇</option>
<option>支出</option>
<option>收入</option>
</select>
</div>
</div>
<div class="form-group">
<label for="money" class="col-sm-2 control-label">金額</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="money" placeholder="金額">
</div>
</div>
<div class="form-group">
<label for="date" class="col-sm-2 control-label">日期</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="date" placeholder="日期">
</div>
</div>
<div class="form-group">
<label for="description" class="col-sm-2 control-label">備註</label>
<div class="col-sm-10">
<textarea class="form-control" id="description"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-8 col-sm-4">
<button type="submit" class="btn btn-default">送出</button>
</div>
</div>
</form>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-bordered table-hover">
<tr>
<th>#</th>
<th>類別</th>
<th>日期</th>
<th>金額</th>
</tr>
<tr>
<td>1</td>
<td>支出</td>
<td>2016-01-01</td>
<td>300</td>
</tr>
<tr>
<td>2</td>
<td>支出</td>
<td>2016-01-02</td>
<td>1,600</td>
</tr>
<tr>
<td>3</td>
<td>支出</td>
<td>2016-01-03</td>
<td>8,00</td>
</tr>
</table>
</div>
</div>
定義 ViewModel 以及 Child Action 用以呈現歷史清單內容
ViewModel 如下所示:
public class AccountingViewModel
{
public int Amount { get; set; }
public DateTime Date { get; set; }
public string Remark { get; set; }
public AccountingType Type { get; set; }
}
public enum AccountingType
{
收入,
支出
}
在 AccoutingController 中增加一個 Child Action 方法,叫做 ShowHistory(),將寫死的 ViewModel 集合傳入 View() 後回傳,內容如下:
[ChildActionOnly]
public ActionResult ShowHistory()
{
var history = new List<AccountingViewModel>
{
new AccountingViewModel {Type=AccountingType.收入, Amount=100, Date = new DateTime(2016,4,10), Remark="發傳單" },
new AccountingViewModel {Type=AccountingType.支出, Amount=40, Date = new DateTime(2016,4,11), Remark="咖啡" },
};
return View(history);
}
[ChildAction]
代表這個 action 只能給 Child Action 使用產生 Child Action 的 View
在 ShowHistory()
上,再透過 Add View 來產生 Child Action 的 View 。設定如下:
- Template 的部分,選擇 List ,因為我們要呈現多筆歷史資料。
- Model class 的部分,選擇剛剛新增的 ViewModel
- 不套用 Layout, 因為這是 ChildAction 的 View
ShowHistory.cshtml 的內容,移除 <body> 以外的內容,只保留 content 的部分,內容如下:
@model IEnumerable<MyMoney.Models.ViewModels.AccountingViewModel>
@{
Layout = null;
}
<table class="table table-bordered table-hover">
<tr>
<th>#</th>
<th>
@Html.DisplayNameFor(model => model.Amount)
</th>
<th>
@Html.DisplayNameFor(model => model.Date)
</th>
<th>
@Html.DisplayNameFor(model => model.Remark)
</th>
</tr>
@{ var index = 1;}
@foreach (var item in Model)
{
<tr>
<td>@(index)</td>
<td>
@Html.DisplayFor(modelItem => item.Amount)
</td>
<td>
@Html.DisplayFor(modelItem => item.Date)
</td>
<td>
@Html.DisplayFor(modelItem => item.Remark)
</td>
</tr>
index++;
}
</table>
接著把 Index.cshtml 中要呈現歷史清單的部分,改使用 HtmlHelper 的 Action,關鍵程式碼如下所示:
<div class="row">
<div class="col-md-12">
@Html.Action("ShowHistory")
</div>
</div>
瀏覽 Accounting/Index 時如下圖所示:
微調歷史清單呈現,貼近需求
清單的部分,金額要千分位(三位一撇),日期的部分希望只呈現日期,不需要呈現時間。這邊只需要在 ViewModel 上透過 DisplayFormat裡面的 DataFormatString 以類似 String Format 的方式設定即可。
public class AccountingViewModel
{
[DisplayFormat(DataFormatString = "{0:N0}")]
public int Amount { get; set; }
[DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}")]
public DateTime Date { get; set; }
public string Remark { get; set; }
public AccountingType Type { get; set; }
}
畫面如下所示:
這時歷史清單的欄位名稱還是 ViewModel 的 property name, 所以接著調整 ViewModel 的 Display attribute 裡面 Name 的設定,如下所示:
public class AccountingViewModel
{
[DisplayFormat(DataFormatString = "{0:N0}")]
[Display(Name = "金額")]
public int Amount { get; set; }
[DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}")]
[Display(Name ="日期")]
public DateTime Date { get; set; }
[Display(Name ="備註")]
public string Remark { get; set; }
[Display(Name ="類別")]
public AccountingType Type { get; set; }
}
在 ShowHistory.cshtml 中,也把類別的欄位與內容加進去,最後畫面如下所示:
結論
這大概就是第一天作業內容的範圍,也與網站開發實務的順序很貼近。摘要整理一下:
- 拿到 designer 給的 html
- 貼到 layout
- 決定 route 的 controller 與 action 名稱
- 產生 view
- 將 layout 中每一頁獨自內容的部分,搬到剛剛產生的 view 裡面,把 layout 抽出來的部分,改成
@RenderBody()
- 將查詢清單的職責定義為 Child Action (以這例子也可以使用 Partial View) ,可透過
[ChildActionOnly]
標記,讓這個 action 只給 child action 使用。 - 定義查詢清單每一筆資料的 ViewModel 進而產生 Child Action 的 View
- 調整 Child Action 的 View,並調整原 View 中要呈現 Child Action 內容的部分,改成
@Html.Action()
- ViewModel 的 property 可透過 Display(Name="") 與 DisplayFormat(DataFormatString="") 來搭配 HtmlHelper 調整 View 的呈現方式。例如
@Html.DisplayNameFor()
與@Html.DisplayFor()
雖然這篇文章只是入門款,但開發 ASP.NET MVC 一開始起手式的核心概念,在這個 homework 表露無遺。當然目前完成的功能還不完整,在後續幾篇文章,將陸陸續續補上其他的概念。
blog 與課程更新內容,請前往新站位置:http://tdd.best/