ASP.NET MVC 3: โครงสร้างของ MVC โดยใช้ Razor

เกริ่นต่อ …

อธิบายต่อจาก บทความ ASP.NET MVC 3: แนะนำให้รู้จัก ครับเมื่อเราสร้าง ASP.NET MVC3 โดยเลือกใช้ Razor เป็น View Engine แล้ว ด้วย Visual Studio 2010 จะเตรียมโครงสร้าง projects ให้เราเสร็จพร้อมพัฒนาเพิ่มเติมได้ มีโครงสร้างหน้าตาแบบนี้ครับ

อธิบายโครงสร้าง folder ต่างๆ ดังนี้

Content— ที่เก็บ Static files เช่น CSS หรือ รูปภาพ, media file ต่างๆ
Controllers— ที่เก็บ Controller classes ของ application
Models— ที่เก็บ Domain Models หรือ Business Models ของ application
Scripts—ที่เก็บ file JavaScript library
Views— ที่เก็บ views template หรือส่วนนำเสนอของ application (ASP.NET MVC3 ที่มี razor view-engine ตัวใหม่ให้เลือกใช้ จะสร้าง view file  *.cshtml หรือ *.vbhtml แทน *.aspx ที่เป็นแบบเดิมทั้งหมด)

เมื่อลองวาด Controller, Models และ View ด้วย class Diagram จะทำให้เราเข้าใจความสัมพันธ์ของพวกมันมากขึ้น ตามภาพนี้ครับ (แสดงรูป Domain Account ที่ถูกจัดการ(Organization Domain Models) ให้อยู่ในรูปแบบ MVC แล้ว)

ผมจะอธิบาย Account MVC ถ้าหากว่าคุณเข้าใจ Account MVC ได้ คุณก็จะเข้าใจการประยุกต์ ASP.NET MVC ได้ทุกๆ business domain ได้ง่ายขึ้น

เริ่มจาก models ใน domain Account ที่อยู่ใน files Models/AccountModels.cs ประกอบไปด้วย ChangePasswordModel, LogOnModel และ RegisterModel ซึ่งคุณจะสังเกตุเห็นว่า model ที่สร้างจาก visual studio มาให้นั้น 1 model จะแทน 1 view เท่านั้นซึ่งมันคล้ายๆกับ Naked Objects Pattern คือ 1 view จะแสดง(render) 1 model และเมื่อต้องการแสดง หลายๆ model ใน 1 view คุณจะต้องสร้าง model หนึ่งขึ้นมาที่จะประกอบไปด้วยหลายๆ model ของเรื่องราวที่คุณต้องการนำเสนอที่หน้าจอนั้น

ซึ่งผมคิดว่า Naked Objects Pattern เป็นแนวคิดที่ใกล้เคียงโลกจริง(real world) ได้ค่อนข้างมาก เพราะผมเคยคิดว่า ทำไมด้วยตาของเรายังเห็น domain models เห็นพฤติกรรมของมันแล้วเข้าใจได้ทันที(เช่น เห็นรถเป็นรถ เห็นต้นไม้เป็นต้นไม้ เห็นพฤติกรรมแบบนี้ก็เข้าใจว่ามันทำอะไรอยู่ ทำอะไรได้) นั่นแสดงว่าตัว domain models มันสามารถจะแสดงด้วยตัวของมันเองได้ แล้วทำไม domain models ที่อยู่ใน software application ใดๆของเราจึงจะแสดงตนเอง หรือพฤติกรรมของตนเองออกมาไม่ได้บ้างละ! มันจะต้องมีอะไรสักอย่างที่ทำให้  domain models ของเราสามารถแสดงตัวมันออกมาทั้ง State และ Behavior ได้เองตามที่มันเป็นอยู่ ซึ่งนั่นก็คือแนวคิดของ Naked Objects Pattern นั่นเอง

State(Property หรือ Attribute) ในแต่ละ model ได้ระบุ Data Annotation เพื่อใช้แสดง format ของ state ตนเองไว้ด้วย ซึ่ง Data Annotation จะอยู่ใน library System.ComponentModel.DataAnnotations ผมจะอธิบาย Data Annotation ว่าคืออะไร เพราะมันสำคัญมากต่อการสร้าง view กับการ validate model โดย Html Helper

  • Required: ใช้ระบุ เพื่อบอกว่าต้องการ field นี้ห้ามปล่อยทิ้งว่างไว้ เมื่อมีการ validate ที่ view
  • Display: เมื่อระบุไว้ที่ field ใดๆแล้วมันจะแสดงตามข้อความที่เราระบุ เพื่อแสดงให้ user เห็น ในแบบเข้าใจได้ง่ายขึ้น
  • DataType: ใช้ระบุว่า field นี้เป็นข้อมูลชนิดอะไร เช่น เป็น EmailAddress ซึ่ง validator จะ validate filed นี้ด้วย format ของ email
  • StringLength: ใช้ระบุความยาวของ field ต้องมีความยาวมากน้อยเท่าไรใช้ validate rage ของข้อมูลที่ view
  • Compare: ใช้เปลียบเทียบ field สอง field ว่ามีค่าเหมือนกันหรือไม่ เช่น validate ข้อมูล password กับ confirm password ที่กรอกใน view ต้องเหมือนกัน

* validate คือการตรวจสอบข้อมูลก่อนที่จะดำเนินการต่อในขั้นตอนต่อไป

เราลองเปิดดูสัก model กันดีกว่า ผมเปิด file AccountModels.cs ไปที่ class RegisterModel code มันมีหน้าตาแบบนี้ครับ

public class RegisterModel

{

[Required][Display(Name = “User name”)]

public string UserName { get; set; }

[Required][DataType(DataType.EmailAddress)]

[Display(Name = “Email address”)]

public string Email { get; set; }

[Required][StringLength(100, ErrorMessage = “The {0} must be at least {2} characters long.”, MinimumLength = 6)]

[DataType(DataType.Password)][Display(Name = “Password”)]

public string Password { get; set; }

[DataType(DataType.Password)][Display(Name = “Confirm password”)]

[Compare(“Password”, ErrorMessage = “The password and confirmation password do not match.”)]

public string ConfirmPassword { get; set; }

}

คุณจะเห็นว่า เมื่อผู้ใช้ register ผู้ใช้จะต้องกรอก field ที่มี Annotation Required ระบุอยู่ ได้แก่ field UserName, Email และ Password ลอง run application ขึ้นมาแล้วเข้าไป กรอกข้อมูล RegisterModel ใน form นี้กันเลย

ไปที่ projects ของเราครับแล้วกด F5 หรือกดที่ปุ่ม run เมื่อ application run ขึ้นมาปรากฏที่ browser แล้ว พิมพ์ /Account/Register ต่อเพิ่มเข้าไปที่ url แล้วกด enter

จะขึ้นหน้าจอ register ไม่ต้องกรอกอะไรกดปุ่ม Register ครับ หน้าจอจะ validate register model ให้เรา แล้วบ่นออกมาแบบนี้

ถึงตรงนี้เราก็คงสงสัยว่า มันสร้าง view มาจาก model ได้อย่างไรใช่มั้ยครับ เราไปเปิด file Register.cshtml ที่เป็น view ของหน้า register นี้ขึ้นมา ก็เจอ code แบบนี้

@model CustomersManagmentApplication.Models.RegisterModel

@{

ViewBag.Title = “Register”;

}

อธิบาย code ด้านบน บรรทัดแรกที่อยู่ใน file Register.cshtml  ก็เจอสัญลักษ์แปลกๆนี้ @ มันคือสัญลักษ์ Razor code block ครับ ใช้เมื่อเราต้องการเขียน code C#/VB.NET เพื่อแสดง logic การนำเสนอ model หรือ logic นำเสนออะไรลงไปใน file view ได้โดยให้เราขึ้นต้นด้วย @ ในแบบ code บรรทัดเดียว และ เขียนแบบนี้ @{… … } เมื่อต้องการแสดง code หลายๆบรรทัด ซึ่งใน ASP.NET แบบมาตรฐานดั้งเดิมจะแสดงสัญลักษ์ code block แบบ code บรรทัดเดียวด้วย <%= … %> และแบบ code หลายๆบรรทัดด้วย <% …;…; %>

การใช้สัญลักษ์ razor นอกจากจะใช้ Razor view engine ตัวใหม่ที่เร็วขึ้นแล้ว ยังทำให้เขียน code สั้นลง อ่านง่ายขึ้น และมีความคล่องตัวมากขึ้นในการทำ unit testing หรือสร้าง HTML Helper extension เพื่อเขียน tag/javascript หรืออะไรต่อมิอะไรตามใจเราได้อีกด้วย

ดู code @model  มันคือการระบุชนิด model ให้ file view ในแบบ strong type จาก code กำหนดให้ class CustomersManagmentApplication.Models.RegisterModel เป็น model ที่ render ใน view นี้ครับ นั่นหมายความว่า view นี้เพื่อนำเสนอ model RegisterModel เท่านั้นนะ model อื่นฉันไม่รู้จัก

code บรรทัดถัดมาเจอ @ เพื่อเปิด code block แล้วกำหนดให้ ViewBag.Title = “Register” โดย ViewBag เป็นตัวแปรแบบ Dynamic ที่มันสามารถกำหนด publish field ชื่ออะไรเข้าไปก็ได้ ในที่นี้เป็นกำหนด Title ซึ่งข้อความนี้ มันจะถูกนำไปแสดงที่ Title ของ Layout หลักของ web(แบบไม่ใช้ Razor จะใช้ Master page เป็น Layout) ที่อยู่ใน file Shared/_Layout.cshtml เมื่อเปิดดูแล้ว เราจะเห็น code ที่นำ  ViewBag.Title ไปแสดงที่ title ของ browser แบบนี้

<title>@ViewBag.Title</title>

เรามาที่ file view Register.cshtml กันต่อดีกว่า เลื่อนลงมาข้ามๆ code ที่ไม่สำคัญไป ถึงตรง code หน้าตาแบบนี้

<script src=”@Url.Content(“~/Scripts/jquery.validate.min.js”)” type=”text/javascript”></script>
<script src=”@Url.Content(“~/Scripts/jquery.validate.unobtrusive.min.js”)” type=”text/javascript”></script>

code ข้างต้นได้ include JavaScript ที่อยู่ใน folder Scripts เข้ามาใน file view นี้ โดยคุณจะเห็นสัญลักษณ์ @ เพื่อเปิด block code โดยใช้ Url.Content เขียนอ้างถึง path ของ file library jQuery ที่เกี่ยวกับการสร้างกลไก validate เข้ามา 2 file (อ่านเพิ่มเติมการใช้ plugin jQuery validate จากบทความ ASP.NET กับ jQuery: ใช้ jQuery validation plugin ใน UserLogin form ง่ายๆ)

@using (Html.BeginForm()) { … }

code ข้างบน จะสร้าง HTML form แบบนี้ <form action=”/Account/Register” method=”post”> …. </form> ให้เราจาก method BeginForm ดังนั้น code ที่อยู่ใน block { … } จะถูกเขียนเข้าไปใน form เมื่อดูให้ดีจะเห็นว่ามันจะสร้าง method post action เท่ากับ /Account/Register ซึ่งการ post นี้จะไปถึง method Register ของ AccountController ที่จะอธิบายต่อไปหลังจากอธิบายส่วน view เสร็จแล้ว

ต่อไปจะเป็น code ส่วนที่ สร้าง view จาก model RegisterModel ใน file Register.cshtml

@using (Html.BeginForm()) {

@Html.ValidationSummary(true, “Account creation was unsuccessful. Please correct the errors and try again.”)

<div>

<fieldset>

<legend>Account Information</legend>

<div class=”editor-label”>@Html.LabelFor(m => m.UserName)</div>

<div class=”editor-field”>

@Html.TextBoxFor(m => m.UserName)

@Html.ValidationMessageFor(m => m.UserName)

</div>

<divclass=”editor-label”>@Html.LabelFor(m => m.Email)</div>

<divclass=”editor-field”>

@Html.TextBoxFor(m => m.Email)

@Html.ValidationMessageFor(m => m.Email)

</div>

<div class=”editor-label”>@Html.LabelFor(m => m.Password)</div>

<divclass=”editor-field”>

@Html.PasswordFor(m => m.Password)

@Html.ValidationMessageFor(m => m.Password)

</div>

<div class=”editor-label”>@Html.LabelFor(m => m.ConfirmPassword)</div>

<div class=”editor-field”>

@Html.PasswordFor(m => m.ConfirmPassword)

@Html.ValidationMessageFor(m => m.ConfirmPassword)

</div>

<p>

<input type=”submit”value=”Register”/>

</p>

</fieldset>

</div>

}

จะเห็นว่ามีการใช้ Html helper สร้างส่วนนำเสนอ model ขึ้นมาแทนที่จะเขียนเอง ซึ่งมันจะสร้าง view และการ validate ข้อมูลที่ขึ้นอยู่กับ Data Annotation ของ model ที่ได้นำเสนอไปแล้วนั้นด้วย เทคนิคสำคัญนี้นี่เองที่ทำให้เราเขียน code ส่วนนำเสนอ การ validate model จากทั้งฝั่ง client และ server น้อยลงไปมากเลยทีเดียว

นี่คือ tag HTML ที่สร้างจาก Html helper บางส่วน (การใส่ logic ใดๆเข้าไปใน markup แบบนี้เรียกว่า Unobtrusive JavaScript )

<input data-val=”true” data-val-required=”The User name field is required.” id=”UserName” name=”UserName” type=”text” value=”” />

สร้างจาก @Html.TextBoxFor(m => m.UserName) หรือในแบบไม่ใช้ Lambda expressions เขียนแบบนี้ @Html.TextBox(“UserName”)

และ

<span data-valmsg-for=”UserName” data-valmsg-replace=”true”></span>

สร้างจาก @Html.ValidationMessageFor(m => m.UserName) หรือในแบบไม่ใช้ Lambda expressions เขียนแบบนี้ @Html.ValidationMessage(“UserName”) (แนะนำให้เขียนแบบ Lambda expressions เพราะ compiler จะช่วยเรา validate ในกรณีที่ property ของ model ถูกเปลี่ยน หรือ refactor จะทำงาน rename ให้เราด้วยโดยอัตโนมัติได้)

ซึ่งเมื่อกลับไปเปิด RegisterModel ดูก็จะเห็น message ที่กำหนดใน Annotation ของ field ต่างๆถูก Html helper นำมาสร้างวัตถุ HTML ในส่วนนำเสนอ และสร้าง validator ที่ฝั่ง client ให้ด้วยโดยอัตโนมัติ ซึ่งมันทำให้การ validate rule ใน model ของเราไม่กระจัดกระจายไปอยู่ใน view อื่นๆมากมาย แต่เราฝังมัน(embened rule) ไว้ใน model เลย เมื่อเราจำเป็นต้องกลับไปแก้ไข rule ใหม่ก็ไม่จำเป็นต้องไปตามแก้ code ที่ view และที่อื่นๆ ที่มีการ validate rule ของ model นี้อีกต่อไปแต่แก้ไขที่ model ที่เดียวจบข่าว

ผมก็ขอจบในส่วนของ view เพียงเท่านี้ครับ

เรากลับขึ้นไปดูที่ action=”/Account/Register” กันเพื่อจะได้โยงไปถึงส่วนของ Controller ได้เลย จาก view register คุณจะเห็นว่ามีปุ่มที่มันจะ submit form ทั้งหมดแล้ว post ไปยัง server ไปถึง AccountController ก็จะรับ request นี้ไป ด้วย method ที่ตรงกับสิ่งที่ post action มาเมื่อเราเปิดดูที่ file controller AccountController.cs เราก็จะพบ method นี้ครับ

// POST: /Account/Register

[HttpPost]

publicActionResult Register(RegisterModel model)

{

if (ModelState.IsValid)

{

// Attempt to register the user

MembershipCreateStatus createStatus;

Membership.CreateUser(model.UserName, model.Password, model.Email, null, null, true, null, out createStatus);

if (createStatus == MembershipCreateStatus.Success)

{

FormsAuthentication.SetAuthCookie(model.UserName, false/* createPersistentCookie */);

return RedirectToAction(“Index”, “Home”);

}

else

{

ModelState.AddModelError(“”, ErrorCodeToString(createStatus));

}

}

// If we got this far, something failed, redisplay form

return View(model);

}

จะเห็นว่าเมื่อ view Register post form ไปยัง server server จะรับ request นี้ แล้วส่งต่อไปยัง AcountController ที่ method action Register เพื่อทำงานได้อย่างถูกต้อง

ทดลอง ลงทะเบียนดูครับคุณจะเห็นว่า คุณสามารถลงทะเบียนได้จริงโดย ข้อมูลที่คุณกรอกลงไป จะถูกบันทึกไว้ที่ table aspnet_Membership ในฐานข้อมูลที่อยู่ใน file ASPNETDB.MDF ซึ่งอยู่ใน folder App_Data แต่ต้องกด Show All file จึงจะเห็นครับ ตามภาพ

พอเห็น file ASPNETDB.MDF แล้วให้ double click มันเลยครับ คุณจะพบกับ table aspnet_Membership ที่จะเก็บ state บางค่าจาก RegisterModel ตอนที่เรากด Register ที่หน้าจอ Register มันจะ post action -> Account -> Register พร้อมกับข้อมูลที่ validate เรียบร้อยแล้วบน form ไปยัง method Register ที่อยู่ใน AccountController ที่ code บรรทัดนี้

Membership.CreateUser(model.UserName, model.Password, model.Email, null, null, true, null, out createStatus);

จะสร้าง Membership แล้วเก็บลงใน table aspnet_Membership ตามภาพข้างล่างครับ

ผมขอจบบทความ ASP.NET MVC 3: โครงสร้างของ MCV3 ในแบบ Razor ไว้เพียงเท่านี้

บทความต่อไป ผมจะมาทำ Application CustomerManagement นี้ต่อตามที่ได้สร้าง projects ไว้แต่แรก ด้วยวิธี Code-First Development ครับ ซึ่งอ่านเตรีมความรู้ไว้ก่อนก็ได้ที่ Code-First Development ด้วย Entity Framework

ขอบคุณครับ 🙂

บทความต่อไป: ASP.NET MVC 3: Code-First Development และ Scaffolding CRUD interface

Advertisements

#net, #asp-net, #pattenrs