08 Controller 介紹 - wycmaker/MVC-learning GitHub Wiki
ASP.NET MVC 的核心在於Controller,負責網站流程相關的工作,當用戶送出請求後,經由APP_Start資料夾中RouteConfig.cs
對於Route的定義,將路由解析後丟給對應的Controller與Action處理,判斷Action是否需要向Model要資料,處理完後將工作交給對應的View,可以說幾乎所有網站的請求都會經由Controller處理。
Controller為一個類別,其中包含許多Method,如果Method設為公開則會被認定為Action或Action Method,不設為公開則被當成一般方法。
撰寫Controller的注意事項:
-
檔案名稱必須用Controller結尾
-
Controller的類別必須設為公開
-
必須繼承ASP.NET MVC內建的Controller類別,或其他有實作IController介面的類別
-
Action Method必須宣告為
public
透過ActionName(名稱)
來設定,預設會執行Controller中的Index
Action,但是我們將名稱設為Default,所以在路由的Action參數必須是Default才會執行Index
Action。
[ActionName("Default")]
public ActionResult Index()
{
return View();
}
所以當我們網址輸入https://localhost:44381/GuestBooks/Index
,會顯示找不到資源
由於我們套用了名稱選擇器,將Action名稱改為Default,在return View()
的時候,對應到的是Default.cshtml
,而不是Index.cshtml
,所以網址輸入https://localhost:44381/GuestBooks/Default
也會產生錯誤
-
NonAction
:在Controller類別中,所有宣告為public的方法都會被視為Action,如果我們想宣告public的方法,卻又不讓方法成為Action,可以使用NonAction
屬性,也等同於將方法從public改成private
[NonAction]
public ActionResult ExistContent(int id)
{
if (Convert.ToBoolean(db.GuestBooks.Find(id))) return Content("true");
else return Content("false");
}
//等同於:
//private ActionResult ExistContent(int id)
//{
// if (Convert.ToBoolean(db.GuestBooks.Find(id))) return Content("true");
// else return Content("false");
//}
-
HttpAction
:其中包刮HttpGet
、HttpPost
、HttpDelete
、HttpPatch
、HttpPut
、HttpHead
、HttpOptions
等屬性,將不同的屬性套用在Action方法上,代表只有Http請求的Method符合屬性時,才會選取到這個Action
// 只有Http請求為Get時,才會選擇此Action方法
// GET: GuestBooks/Details/5
// Post、Delete、Patch、Put都不會選擇此Action方法
[HttpGet]
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
GuestBook guestBook = db.GuestBooks.Find(id);
if (guestBook == null)
{
return HttpNotFound();
}
return View(guestBook);
}
ActionResult是Action方法回傳的型別,回傳資料時,只會回傳需要的資訊,不包刮執行的結果,ActionResult包含許多衍生型別,以下逐一做介紹。
-
ContentResult
: 可以用來回傳文字內容,可以定義文字內容、內容類型與編碼
//回傳純文字內容
public ContentResult Hello()
{
return Content("Hello World!");
}
//另外的寫法1:
public ActionResult Hello()
{
return Content("Hello World!");
}
//另外的寫法2:
public string Hello()
{
return "Hello World!";
}
//回傳Html
//Content(Content, Content-Type, Encoding)
public ContentResult H1Content()
{
return Content("<h1>Hello</h1>", "text/html", System.Text.Encoding.UTF8);
}
-
EmptyResult
:當Action方法沒有要回傳的資訊時,可以使用EmptyResult作為方法的型別
public EmptyResult Count()
{
return null;
}
//另外的寫法1:
public ActionResult Count()
{
return new EmptyResult();
}
//另外的寫法2:
public void Count()
{
return;
}
-
RedirectResult
:作用是重新導向連結,等同於Response.Redirect
,可以進行同專案下的頁面跳轉,也可以跳轉到不同專案或外部網站,使用Redirect
方法是採用Http 302暫時轉向,如果要採用Http 301永久轉向可以使用``
// Http 302暫時轉向
public RedirectResult GoToHello()
{
return Redirect("/GuestBooks/Hello");
}
// Http 301永久轉向
public RedirectResult GoToPermanent()
{
return RedirectPermanent("/GuestBooks/Hello");
}
// 轉向外部網站
public RedirectResult GoToHello()
{
return Redirect("https://www.youtube.com/");
}
-
RedirectToRouteResult
:作用也是重新導向連結,不過僅限於同專案下的轉向,有Http 302暫時轉向的RedirectToAction
與RedirectToRoute
方法,以及Http 302永久轉向的RedirectToActionPermanent
與RedirectToRoutePermanent
方法
// RedirectToAction
public RedirectToRouteResult GoToHello ()
{
return RedirectToAction("Hello", "GuestBooks");
}
// RedirectToActionPermanent
public RedirectToRouteResult GoToPermanent()
{
return RedirectToActionPermanent("Hello", "GuestBooks");
}
// RedirectToRoute
public RedirectToRouteResult Route()
{
return RedirectToRoute(new { controller = "GuestBooks", action = "Hello" });
}
// RedirectToRoutePermanent
public RedirectToRouteResult RoutePermanent()
{
return RedirectToRoutePermanent(new { controller = "GuestBooks", action = "Hello" });
}
-
ViewResult
:回傳指定的View頁面
// 套用Show.schtml
public ViewResult Show()
{
return View();
}
// 設定套用得主板頁面名稱
// View(檢視頁面, 主板頁面)
public ViewResult Show()
{
return View("Show", "_Layout2");
}
-
PartViewResult
:回傳指定的View頁面,但不會套用主板頁面,可以透過AJAX將當前頁面的某些區塊做更換
public PartialViewResult ShowPartial()
{
return PartialView();
}
-
HttpUnauthorizeResult
:回傳一個拒絕存取的錯誤,等於發送一個Http 401的錯誤狀態
public HttpUnauthorizedResult Error()
{
return new HttpUnauthorizedResult();
}
-
HttpNotFoundResult
:回傳一個Http 404的錯誤狀態
public HttpNotFoundResult NotFound()
{
return HttpNotFound("Page Not Exist!");
}
若要使用其他的Http狀態碼,可以使用HttpStatusCodeResult
public HttpStatusCodeResult StatusCode()
{
return new HttpStatusCodeResult(201, "測試成功!");
}
-
JavaScriptResult
: 跟ContentResult一樣回傳文字內容,只是回傳的是JavaScript程式碼,Conttent-Type為application/x-javascript
public JavaScriptResult AlertBox()
{
string js = @"document.getElementById('test').innerText = '測試成功!'";
return JavaScript(js);
}
-
JsonResult
: 回傳一個JSON物件
public JsonResult Json()
{
var result = new
{
id = 1,
name = "jack"
};
return Json(result, JsonRequestBehavior.AllowGet);
}
-
FilePathResult
:繼承自FileResult,會根據檔案路徑回傳一個檔案內容
public FilePathResult FilePath()
{
var path = Server.MapPath("~/Images/ASP9.png");
return File(path, "image/png");
}
-
FileContentResult
:繼承自FileResult,會用二進位資料的方式傳回一個檔案內容,可以透過File方法進行檔案的下載
public FileContentResult FileContent()
{
byte[] data = System.Text.Encoding.UTF8.GetBytes("範例");
// File方法的第三個參數,產生可以下載的檔案
return File(data, "text/plain", "example.txt");
}
-
FileStreamResult
:繼承自FileResult,會用stream的方式傳回一個檔案內容
public FileStreamResult FileStream()
{
var path = Server.MapPath("~/Images/ASP9.png");
var filestream = new System.IO.FileStream(path, System.IO.FileMode.Open);
return File(filestream, "image/png", "範例.png");
}
上面的所有Result型別,都是ActionReault的衍生型別,所以在宣告Action方法的時候,宣告為ActionResult型別,可以利用各種不同的Result進行處理,不會侷限在同一個Result型別下
-
ViewData
:為字典型別,資料只存在一次Http請求中,也就是只能對一個Action起作用,導向頁面後ViewData
中的資料就會清空。此外,ViewData
有Model屬性,可以將Controller取得的Model資料傳給View,在View先用@model
宣告頁面用的資料模型型別,在用@Model
取得需要的屬性
Controller:
// 普通用法:
ViewData["Message"] = "ViewData test!";
// ViewData.Model:
var data = db.GetMessages();
ViewData.Model = data;
return View();
View:
// 普通用法:
<h1>@ViewData["Message"]</h1>
// ViewData.Model:
@model MessageBoard.Models.Message
<h3>@Model.Title</h3>
-
ViewBag
:與ViewData
類似,型別為動態型別,使用時不需進行型別轉換
Controller:
ViewBag.Message = "Hello";
View:
<h1>@ViewBag.Message</h1>
注意:若是ViewData
與ViewBag
同時使用,必須注意命名,若使用相同名稱,後面的資料會覆蓋掉前面的資料
Controller:
// 將ViewData與ViewBag都設為Message:
ViewData["Message"] = "ViewData test!";
ViewBag.Message = "Hello";
View:
<h1>@ViewData["Message"]</h1>
<h1>@ViewBag.Message</h1>
<h1>@ViewData["Message"]</h1>
結果:因為ViewBag.Message
設為Hello
,一併影響到ViewData["Message"]
的值,所以顯示出來都是Hello
-
TempData
:與ViewData
一樣都是字典型別,為TempDataDictionary
型別,資料暫存在Session中,所以不僅限於同個Action的傳送,可以做到跨Action的資料傳送,一樣只能維持一個Http請求
Controller:
public ActionResult Index()
{
ViewData["Message"] = TempData["Message"];
var data = db.GetMessages();
ViewData.Model = data;
return View();
}
public ActionResult TempTest()
{
TempData["Message"] = "Temp Test!";
return RedirectToAction("Index");
}
View:
<h1>@ViewData["Message"]</h1>
@Html.ActionLink("TempTest", "TempTest")
結果: 第一次進入頁面因為沒有設定TempData
所以不會顯示字串,按下連結經過RedirectToAction
之後,就可以看到字串了
第一次進入:
第二次進入:
不同於傳統ASP.NET採用Resquest.Form()
取得資料,在ASP.NET MVC中會使用模型繫結(Model Binding)來取得資料,大部分的資料型態都能處理,如果有特殊需求,也可以自己實作繼承IModelBinger
介面的類別
### 簡單的模型繫結 — 表單範例
Controller: 宣告一個參數,將它放入ViewBag.Content
中
public ActionResult ModelBind(string content)
{
ViewBag.Content = content;
return View();
}
View:表單送出後,name為content的輸入框的值,會做為Controller的Action的參數
<form action="/GuestBooks/ModelBind" method="post">
<div>
輸入內容:<input type="text" name="content" />
</div>
<div>
<input type="submit" value="送出" />
</div>
</form>
<div>
<p>你的輸入為:@ViewBag.Content</p>
</div>
結果:
送出表單後
如果表單的輸入欄位不只一個,除了可以使用多個參數外,也可以自己定義一個類別,用來當作表單資料的接收
Model:
public class FormModel
{
public string Content { get; set; }
public string Username { get; set; }
}
Controller:
public ActionResult FormTest( FormModel form)
{
ViewBag.Content = form.Content;
ViewBag.Name = form.Username;
return View();
}
View:
<h2>FormModel</h2>
<form action="/Form/FormTest" method="post">
<div>
內容:<input type="text" name="content" />
</div>
<div>
發言者:<input type="text" name="username" />
</div>
<input type="submit" value="發言" />
</form>
<h1>留言</h1>
<p>留言內容:@ViewBag.Content</p>
<p>By @ViewBag.Name</p>
結果:
按下留言:
如果我們在Model類別的設計上有加入資料的驗證,ASP.NET MVC在進行資料繫結的過程,會一起處理Model的資料驗證工作,當Controller完成資料繫結後,會得到ModelState
物件,可以透過ModelState.IsValid
的狀態判斷輸入的資料是否有通過驗證,透過上一個範例來說明ModelState.IsValid
的用法
Model:
public class FormModel
{
[Required]
public string Content { get; set; }
[Required]
public string Username { get; set; }
}
Controller:
public ActionResult FormTest( FormModel form)
{
if (!ModelState.IsValid)
{
ViewBag.Message = "未通過驗證";
return View();
}
else
{
ViewBag.Message = "通過驗證";
ViewBag.Content = form.Content;
ViewBag.Name = form.Username;
return View();
}
}
View:
<h2>FormModel</h2>
<form action="/Form/FormTest" method="post">
<div>
內容:<input type="text" name="content" />
</div>
<div>
發言者:<input type="text" name="username" />
</div>
<input type="submit" value="發言" />
</form>
<h1>留言</h1>
<p>驗證狀態:@ViewBag.Message</p>
<p>留言內容:@ViewBag.Content</p>
<p>By @ViewBag.Name</p>
結果:
送出留言:
-
ModelState.Count
:可以取得資料繫結過程中驗證的屬性數量 -
取得錯誤訊息:可以使用
ModelError
類別中的ErrorMessage
與Exception
屬性 -
ModelState.AddModelError()
:可以自行增加驗證失敗的訊息 -
ModelState.Clear()
:如果不希望將ModelState的驗證訊息傳到View,可以使用這個方法清空資料繫結的狀態
當類別定義的屬性有很多時,有些屬性不適合在當下做更改,這時候可以用Bind來限制需要驗證的屬性,或是過濾不需要驗證的屬性
-
Exclude
:使用Bind[Exclude = "屬性名稱"]
可以過濾不需要驗證的屬性 -
Include
:使用Bind[Include = "屬性名稱"]
可以指定只有那些屬性需要通過驗證
UpdateModel
可以將表單定義的輸入欄位自動對應至Model的屬性,也可以自己定義那些欄位需要或不需要進行驗證,在Model有許多欄位時會比較方便,可以減少Action的複雜度,但是當驗證失敗的時候,UpdateModel
方法會產生例外狀況,所以通常會使用TryUpdateModel
產生的布林值性行if判斷的處理
public ActionResult FormTest(FormModel form)
{
if(!TryUpdateModel<FormModel> (form))
{
ViewBag.Message = "未通過驗證";
return View();
}
else
{
form.CreateTime = DateTime.Now;
ViewBag.Message = "通過驗證";
ViewBag.Content = form.Content;
ViewBag.Name = form.Username;
ViewBag.Time = form.CreateTime;
return View();
}
}
在ASP.NET MVC 中,提供了Action過濾器,可以附加在特定的Action上,或是特定Controller的所有Action上,讓開發者在執行Action之前或之後,可以定義一些邏輯或意外狀況的處理,過濾器的使用方式有三種
-
在
App_Start
中定義全域使用的過濾器 -
在Action或Controller開頭加上過濾器屬性
-
藉由Controller實作的
IActionFilter
介面,透過覆寫Controller中的方法來加上過濾器
在Action執行前,被用來進行身分的驗證或檢查,底下有許多個不同的屬性可以互相作用
- Authorize屬性:如果沒有附加其他條件,可以進行簡單的登入過濾,讓某些Controller或Action必須登入才可以使用,加上Users可以限制哪些使用者可以進入,加上Roles可以限制些角色可以使用
// 用法說明:
// 簡單的登入過濾,在FormController底下的所有Action都必須登入才能使用
[Authorize]
public class FormController : Controller
// 只有使用者是Jack或Merry才會執行
[Authorize(Users = "Jack,Merry")]
public ActionResult Edit(int id)
{
return View();
}
// 使用者的角色是Admin才會執行
[Authorize(Roles = "Admin")]
public ActionResult Change()
{
return View();
}
- AllowAnonymous屬性:通常與Authorize屬性進行搭配,可以在定義了Authorize屬性的Controller的特定Action使用,讓特定的Action可以不用登入就能執行該Action
// 不用登入就可以檢視Index頁面
[AllowAnonymous]
public ActionResult Index()
{
return View();
}
- ChildActionOnly屬性:只能進行Child Action的呼叫,通常被用來嵌入在HTML裡的部分檢視,ChildAction的呼叫是透過View中的HtmlHelper類別中的Action或RenderAction方法來呼叫
// 只能被當成Child Action呼叫
[ChildActionOnly]
public ActionResult ChangeMessage()
{
return PartialView();
}
- RequireHttps屬性:使Action強制使用Https的方式傳送,原本是使用Http的方式,也會被轉為Https的方式重新傳送
[RequireHttps]
public ActionResult Login()
{
return View();
}
-
ValidateAntiForgeryToken屬性:用來驗證來源的網站是否相同,可以避免駭客進行跨網站的請求偽造,在View中必須使用
@Html.AntiForgeryToken()
產生防偽的隱藏欄位 -
ValidateInput屬性:ASP.NET預設會驗證表單內輸入的資料,阻擋惡意的程式碼或是HTML標籤,所以當輸入必須擁有HTML標籤的時候,可以透過這個屬性來關閉此項驗證
[ValidateInput(false)]
public ActionResult Create(string content)
{
return Content(content);
}
是在Action執行的前後進行額外的處理,有OnActionExecuting
與OnActionExecuted
兩個事件
-
AsyncTimeout屬性:用來設定非同步Action方法的逾期時間,預設為45秒
-
NoAsyncTimeout屬性:讓非同步Action方法的逾期時間為無限大
有OnResultExcuting
與OnResultExecuted
兩個事件,在ActionResult執行的前後還未回傳客戶端時,可以進行一些邏輯判斷或是修改輸出的結果
- OutputCache屬性:用來實作ASP.NET MVC的快取機制,可以透過Duration屬性設定取得快取的時間長短
於程式執行階段發生例外狀況之後,替例外狀況進行的處裡,通常被用來進行錯誤的記錄或是錯誤發生時的頁面導向
- HandleError屬性:在程式執行階段發生例外狀況時,就會顯示錯誤檢視
書籍:ASP.NET MVC 4 開發實戰
書籍:一次就懂 ASP.NET MVC 5.x 網站開發:Web應用的經典實務範例解析(Visual C# )