08 Controller 介紹 - wycmaker/MVC-learning GitHub Wiki

Controller職責

ASP.NET MVC 的核心在於Controller,負責網站流程相關的工作,當用戶送出請求後,經由APP_Start資料夾中RouteConfig.cs對於Route的定義,將路由解析後丟給對應的Controller與Action處理,判斷Action是否需要向Model要資料,處理完後將工作交給對應的View,可以說幾乎所有網站的請求都會經由Controller處理。

Controller架構

Controller為一個類別,其中包含許多Method,如果Method設為公開則會被認定為Action或Action Method,不設為公開則被當成一般方法。

撰寫Controller的注意事項:

  1. 檔案名稱必須用Controller結尾

  2. Controller的類別必須設為公開

  3. 必須繼承ASP.NET MVC內建的Controller類別,或其他有實作IController介面的類別

  4. Action Method必須宣告為public

Controller的執行過程

Action選取器

Action名稱選取器

透過ActionName(名稱)來設定,預設會執行Controller中的IndexAction,但是我們將名稱設為Default,所以在路由的Action參數必須是Default才會執行IndexAction。

[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也會產生錯誤

Action方法選擇器

  • 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:其中包刮HttpGetHttpPostHttpDeleteHttpPatchHttpPutHttpHeadHttpOptions等屬性,將不同的屬性套用在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介紹

ActionResult是Action方法回傳的型別,回傳資料時,只會回傳需要的資訊,不包刮執行的結果,ActionResult包含許多衍生型別,以下逐一做介紹。

  1. 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);
}
  1. EmptyResult:當Action方法沒有要回傳的資訊時,可以使用EmptyResult作為方法的型別
public EmptyResult Count()
{
    return null;
}
//另外的寫法1:
public ActionResult Count()
{
    return new EmptyResult();
}
//另外的寫法2:
public void Count()
{
    return;
}
  1. 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/");
}
  1. RedirectToRouteResult:作用也是重新導向連結,不過僅限於同專案下的轉向,有Http 302暫時轉向的RedirectToActionRedirectToRoute方法,以及Http 302永久轉向的RedirectToActionPermanentRedirectToRoutePermanent方法
// 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" });
}
  1. ViewResult:回傳指定的View頁面
// 套用Show.schtml
public ViewResult Show()
{
    return View();
}
// 設定套用得主板頁面名稱
// View(檢視頁面, 主板頁面)
public ViewResult Show()
{
    return View("Show", "_Layout2");
}
  1. PartViewResult:回傳指定的View頁面,但不會套用主板頁面,可以透過AJAX將當前頁面的某些區塊做更換
public PartialViewResult ShowPartial()
{
    return PartialView();
}
  1. HttpUnauthorizeResult:回傳一個拒絕存取的錯誤,等於發送一個Http 401的錯誤狀態
public HttpUnauthorizedResult Error()
{
    return new HttpUnauthorizedResult();
}

  1. HttpNotFoundResult:回傳一個Http 404的錯誤狀態
public HttpNotFoundResult NotFound()
{
    return HttpNotFound("Page Not Exist!");
}

若要使用其他的Http狀態碼,可以使用HttpStatusCodeResult

public HttpStatusCodeResult StatusCode()
{
    return new HttpStatusCodeResult(201, "測試成功!");
}
  1. JavaScriptResult: 跟ContentResult一樣回傳文字內容,只是回傳的是JavaScript程式碼,Conttent-Type為application/x-javascript
public JavaScriptResult AlertBox()
{
    string js = @"document.getElementById('test').innerText = '測試成功!'";
    return JavaScript(js);
}
  1. JsonResult: 回傳一個JSON物件
public JsonResult Json()
{
    var result = new
    {
        id = 1,
        name = "jack"
    };
    return Json(result, JsonRequestBehavior.AllowGet);
}
  1. FilePathResult:繼承自FileResult,會根據檔案路徑回傳一個檔案內容
public FilePathResult FilePath()
{
    var path = Server.MapPath("~/Images/ASP9.png");
    return File(path, "image/png");
}
  1. FileContentResult:繼承自FileResult,會用二進位資料的方式傳回一個檔案內容,可以透過File方法進行檔案的下載
public FileContentResult FileContent()
{
    byte[] data = System.Text.Encoding.UTF8.GetBytes("範例");
    // File方法的第三個參數,產生可以下載的檔案
    return File(data, "text/plain", "example.txt");
}
  1. 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、ViewBag、TempData

  • 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>

注意:若是ViewDataViewBag同時使用,必須注意命名,若使用相同名稱,後面的資料會覆蓋掉前面的資料

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的其他使用方法

  • ModelState.Count:可以取得資料繫結過程中驗證的屬性數量

  • 取得錯誤訊息:可以使用ModelError類別中的ErrorMessageException屬性

  • ModelState.AddModelError():可以自行增加驗證失敗的訊息

  • ModelState.Clear():如果不希望將ModelState的驗證訊息傳到View,可以使用這個方法清空資料繫結的狀態

利用Bind限制模型屬性

當類別定義的屬性有很多時,有些屬性不適合在當下做更改,這時候可以用Bind來限制需要驗證的屬性,或是過濾不需要驗證的屬性

  • Exclude:使用Bind[Exclude = "屬性名稱"]可以過濾不需要驗證的屬性

  • Include:使用Bind[Include = "屬性名稱"]可以指定只有那些屬性需要通過驗證

UpdateModelTryUpdateModel

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();
    }
}

Action過濾器屬性

在ASP.NET MVC 中,提供了Action過濾器,可以附加在特定的Action上,或是特定Controller的所有Action上,讓開發者在執行Action之前或之後,可以定義一些邏輯或意外狀況的處理,過濾器的使用方式有三種

  1. App_Start中定義全域使用的過濾器

  2. 在Action或Controller開頭加上過濾器屬性

  3. 藉由Controller實作的IActionFilter介面,透過覆寫Controller中的方法來加上過濾器

Authorization Filters

在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 Filters

是在Action執行的前後進行額外的處理,有OnActionExecutingOnActionExecuted兩個事件

  • AsyncTimeout屬性:用來設定非同步Action方法的逾期時間,預設為45秒

  • NoAsyncTimeout屬性:讓非同步Action方法的逾期時間為無限大

Result Filters

OnResultExcutingOnResultExecuted兩個事件,在ActionResult執行的前後還未回傳客戶端時,可以進行一些邏輯判斷或是修改輸出的結果

  • OutputCache屬性:用來實作ASP.NET MVC的快取機制,可以透過Duration屬性設定取得快取的時間長短

Exception Filters

於程式執行階段發生例外狀況之後,替例外狀況進行的處裡,通常被用來進行錯誤的記錄或是錯誤發生時的頁面導向

  • HandleError屬性:在程式執行階段發生例外狀況時,就會顯示錯誤檢視

參考資料

書籍:ASP.NET MVC 4 開發實戰

書籍:一次就懂 ASP.NET MVC 5.x 網站開發:Web應用的經典實務範例解析(Visual C# )

⚠️ **GitHub.com Fallback** ⚠️