You are on page 1of 42

Ghi nhớ học MVC 5

Thay đổi đơn vị tiền tệ


Bạn có thể thay đổi các thiết lập nền văn hóa cho server của bạn bằng cách thêm một phần ở mục <system.web> trong file Web.config như
thế này: <globalization culture="en-GB" uiCulture="en-GB" />.

Thêm Ninject vào Visual Studio Project

Cách đơn giản nhất để thêm Ninject vào một dự án MVC là sử dụng hỗ trợ được tích hợp của Visual Studio cho NuGet, thứ giúp việc cài đặt
một loạt các gói và giữ cho chúng được cập nhật trở nên dễ dàng. Tôi đã sử dụng NuGet trong Chươ ng 2 để cài đặt thư viện Bootstrap,
nhưng có một danh mục lớn các gói có sẵn, bao gồm cả Ninject.
Chọn Tools Library Package Manager Package Manager Console trong Visual Studio
để mở cửa sổ dòng lệnh NuGet và nhập vào các lệnh sau:

Install-Package Ninject -version 3.0.1.10


Install-Package Ninject.Web.Common -version 3.0.0.7
Install-Package Ninject.MVC3 -Version 3.0.0.6

Lệnh đầu tiên cài đặt các gói cốt lõi của Ninject và số còn lại thì cài đặt phần mở rộng cho lõi khiến Ninject hoạt động tốt với các ứng
dụng ASP.NET (tôi sẽ giải thích ngay sau đây). Đừng hiểu nhầm với phần tham chiếu MVC3 trong tên của gói cuối cùng– nó vẫn hoạt động
tốt với MVC 5.

Sử dụng Ninject
public ActionResult Index()
{
IKernel ninjectKernel = new StandardKernel();
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
IValueCalculator calc = ninjectKernel.Get<IValueCalculator>(); Commented [TMT1]: Thay thế cho
LinqValueCalculator calc = new
LinqValueCalculator();
ShoppingCart cart = new ShoppingCart(calc)
{
Products = products
};

decimal total = cart.CalculateProductTotal();


ViewBag.Total = total;
return View(products);
}
Bước đầu tiên là chuẩn bị Ninject để sử dụng. Để làm điều này, tôi tạo ra một instance của một Ninject kernel, đó là đối tượng có trách
nhiệm giải quyết các phụ thuộc và tạo ra các đối tượng mới. Khi tôi cần một đối tượng, tôi sẽ sử dụng kernel thay vì từ khóa new. Đây là
câu lệnh tạo ra kernel từ bảng liệt kê:

IKernel ninjectKernel = new StandardKernel();

Tôi cần tạo ra một triển khai của interface Ninject.IKernel, bằng cách tạo ra một instance mới của lớp StandardKernel.
Ninject có thể được mở rộng và tùy chỉnh để sử dụng các loại kernel khác nhau, nhưng tôi chỉ cần StandardKernel được tích hợp sẵn
trong chương này. (Trên thực tế, tôi đã được sử dụng Ninject nhiều năm và tôi chỉ cần mỗi StandardKernel).
Bước thứ hai của quá trình này là cấu hình kernel của Ninject để nó hiểu các đối tượng triển khai mà tôi muốn sử dụng cho mỗi
interface mà tôi đang làm việc với. Đây là câu lệnh từ bảng liệt kê mà làm việc đó:

ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();

Ninject sử dụng các tham số kiểu của C # để tạo ra một mối quan hệ: tôi thiết lập interface mà tôi muốn làm việc với như tham số kiểu cho
phương thức Bind và gọi phương thức To trên kết quả nó trả về. Tôi thiết lập các lớp triển khai tôi muốn khởi tạo như tham số kiểu cho
phương thức To. Câu lệnh này nói với Ninject rằng các phụ thuộc trên interface IValueCalculator nên được giải quyết bằng cách
tạo ra một instance của lớp LinqValueCalculator. Bước cuối cùng là sử dụng Ninject để tạo ra một đối tượng, thông qua
phương thức Get của kernel, như thế này:
IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();
Các tham số kiểu sử dụng cho phương thức Get nói với Ninject rằng interface nào mà tôi đang quan tâm và kết quả từ phương thức này là
một instance của kiểu triển khai tôi đã chỉ định với phương thức To lúc nãy.

Tạo ra Dependency Resolver

Thay đổi đầu tiên tôi sẽ làm là tạo ra một custom dependency resolver. MVC Framework sử dụng một dependency resolver để tạo ra các
instance của các lớp mà nó cần phải phục vụ các yêu cầu. Bằng cách tạo ra một custom resolver, tôi có thể đảm bảo rằng MVC Framework
sử dụng Ninject bất cứ khi nào nó tạo ra một đối tượng–bao gồm cả các instance của các controller, chẳng hạn.
Để thiết lập resolver, Tôi đã tạo một thư mục mới có tên là Infrastructure, đó là thư mục mà tôi sử dụng để bỏ các lớp mà không
phù hợp với các thư mục khác trong một ứng dụng MVC. Tôi đã thêm một tập tin lớp mới vào thư mục có tên là
NinjectDependencyResolver.cs, nội dung của nó bạn có thể xem trong
public class NinjectDependencyResolver : IDependencyResolver
{
private IKernel kernel;

public NinjectDependencyResolver(IKernel kernelParam)


{
kernel = kernelParam;
AddBindings();
}

public object GetService(Type serviceType)


{
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
return kernel.GetAll(serviceType);
}

private void AddBindings()


{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
}
}
Lớp dependency resolver của tôi cũng là nơi mà tôi thiết lập ràng buộc Ninject. Trong phương thức AddBindings, tôi sử dụng phương
thức Bind và To để cấu hình mối quan hệ giữa interface IValueCalculator và lớp LinqValueCalculator.

Đăng ký cho Dependency Resolver


Các gói Ninject tôi thêm vào bằng NuGet đã tạo ra một tập tin có tên là NinjectWebCommon.cs trong thư mục App_Start
private static void RegisterServices(IKernel kernel)
{
System.Web.Mvc.DependencyResolver.SetResolver(new
Hoc.Infrastructure.NinjectDependencyResolver(kernel));
}

Tái cấu trúc lại Controller Home


public class HomeController : Controller
{
private IValueCalculator calc;
public HomeController(IValueCalculator calcParam)
{
calc = calcParam;
} Commented [TMT2]: Được thêm vào
private Product[] products = {
new Product {Name = "Kayak", Category = "Watersports", Price =275M},
new Product {Name = "Lifejacket", Category = "Watersports",Price = 48.95M },
new Product {Name = "Soccer ball", Category = "Soccer", Price= 19.50M},
new Product {Name = "Corner flag", Category = "Soccer", Price= 34.95M} };

// GET: Home
public ActionResult Index()
{
/*IKernel ninjectKernel = new StandardKernel();
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();*/ Commented [TMT3]: Bị loại bỏ
ShoppingCart cart = new ShoppingCart(calc)
{
Products = products
};

decimal total = cart.CalculateProductTotal();


ViewBag.Total = total;
return View(products);
}
}

Sử dụng Ràng buộc Có điều kiện


kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>() .WhenInjectedInto<LinqValueCalculator>();

Phương thức Tác dụng


When(predicate) Ràng buộc được sử dụng khi vị từ—một biểu thức lambda—trả về t rue.
WhenClassHas<T>() Ràng buộc được sử dụng khi lớp đang bị inject được chú thích với thuộc tính có kiểu được quy định bởi T.
WhenInjectedInto<T>() Ràng buộc được sử dụng khi lớp đang bị inject vào thuộc kiểu T.

Thiết lập Phạm vi của Đối tượng


using Ninject.Web.Common;
kernel.Bind<IValueCalculator>().To<LinqValueCalculator> ().InRequestScope();
Bảng 6-3. Các Phương thức Phạm vi của Ninject

Tên Tác dụng


InTransientScope() Điều này cũng giống như không đặc tả một phạm vi và tạo ra một đối tượng mới cho mỗi phụ thuộc được
giải quyết. Bảng
6-4.
InSingletonScope() Tạo một inst ance duy nhất được chia sẽ trong suốt toàn bộ ứng dụng. Ninject sẽ tạo inst ance
ToConstant(object) nếu bạn sử dụng InSingletonScope hoặc bạn có thể cung cấp nó với phương thức ToConstant. Các

InThreadScope() Tạo một instance duy nhất được được dùng để giải quyết các phụ thuộc cho các đối tượng được yêu cầu
bởi một thread duy nhất.

InRequestScope() Tạo một instance duy nhất được dùng để giải quyết các phụ thuộc cho các đối tượng được yêu cầu bởi
một yêu cầu HT T P duy nhất .

phương thức xác nhận static trong kiểm thử


Phương thức Mô tả
AreEqual<T>(T, T) AreEqual<T>(T, T, string) Xác nhận rằng 2 đối tượng của kiểu T có cùng giá trị.
AreNotEqual<T>(T, T) AreNotEqual<T>(T, Xác nhận rằng 2 đối tượng của kiểu T không có cùng giá trị.
T, string)
AreSame<T>(T, T) AreSame<T>(T, T, string) Xác nhận rằng 2 biến tham chiếu đến cùng một đối tượng .
AreNotSame<T>(T, T) AreNotSame<T>(T, Xác nhận rằng 2 biến tham chiếu đến các đối tượng khác nhau.
T, string)
Fail() Fail(string) Hủy một xác nhận: không có điều kiện nào được kiểm tra.
Inconclusive() Inconclusive(string) Cho thấy rằng kết quả của kiểm thử đơn vị không thể được lập một cách rõ ràng.
IsTrue(bool) IsTrue(bool, string) Xác nhận rằng một giá trị bool là t rue. Thường được dùng để đánh giá một biểu
thức trả về một kết quả bool.
IsFalse(bool) IsFalse(bool, string) Xác nhận rằng một giá trị bool là false.
IsNull(object) IsNull(object, string) Xác nhận rằng một biến không được gán một đối tượng tham chiếu.
IsNotNull(object) IsNotNull(object, string) Xác nhận rằng một biến được gán một đối tượng tham chiếu.
IsInstanceOfType(object, Type) Xác nhận rằng một đối tượng thuộc một kiểu cụ thể hoặc có nguồn gốc từ một kiểu
IsInstanceOfType(object, Type, string) cụ thể.
IsNotInstanceOfType(object, Type) Xác nhận rằng một đối tượng không thuộc một kiểu cụ thể.
IsNotInstanceOfType(object, Type, string)

Thiết lập DI Container sử dụng Ninject


1. Bắt đầu với việc thêm một thư mục Infrastructure trong SportsStore.WebUI
2. Và class NinjectDependencyResolver.cs, nội dung của nó bạn

public class NinjectDependencyResolver : IDependencyResolver


{
private IKernel kernel;

public NinjectDependencyResolver(IKernel kernelParam)


{
kernel = kernelParam;
AddBindings();
}

public object GetService(Type serviceType)


{
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
try
{
return kernel.GetAll(serviceType);
}
catch (Exception)
{
return new List<object>();
}
}

private void AddBindings()


{
//IValueCalculator là interface để dùng trong controller
//LinqValueCalculator là định nghĩa của interface, khai báo cách thức
//interface hoạt động
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
}
}
3. Tích hợp Ninject trong file NinjectWebCommon.cs
private static void RegisterServices(IKernel kernel)
{
System.Web.Mvc.DependencyResolver.SetResolver(
new SportsStore.WebUI.Infrastructure.NinjectDependencyResolver(kernel));
}

Tạo một Abstract Repository để lấy dữ liệu


Tôi cần một số cách để nhận các thực thể Product từ một database. Như tôi đã giải thích trong Chương 3, model bao gồm việc luận lý học
liên tục cho việc lưu trữ và truy xuất dữ liệu từ các kho dữ liệu liên tục, ngay cả bên trong model. Tôi muốn giữ một mức độ tách biệt giữa
các thực thể data model và nơi lưu trữ và truy xuất lôgic , mà tôi đạt được bằng cách sử dụng repository pattern. Tôi sẽ không lo lắng về việc
làm thế nào mà tôi sẽ thực thi dữ liệu liên tục trong chốc lát, nhưng tôi sẽ bắt đầu quá trình xác định một interface cho nó.
Tạo một thư mục cấp cao mới bên trong project SportsStore.Domain tên là Abstract và bên trong thư mục mới, tạo một file interface mới
tên là IProductsRepository.cs, nội dung của nó thể hiện ở Listing 7-4 . Bạn có thể thêm một interface mới bằng cách kích chuột phải vào thư
mục Abstract, chọn Add New Item, và chọn Interface.

Listing 7-4. Nội dung của file IProductRepository.cs

Bước 1. Tạo ra 1 interface cho việc truy xuất dữ liệu

using System.Collections.Generic;
using SportsStore.Domain.Entities;

namespace SportsStore.Domain.Abstract
{
public interface IProductRepository
{
IEnumerable<Product> Products { get; }
}
}
Inerface này sử dụng IEnumerable<T> cho phép người gọi nhận được một trình tự của các đối tượng Product, mà không cho biết làm thế
nào và nơi nào mà dữ liệu được lưu hay truy xuất. Một lớp mà phụ thuộc vào interface IProductRepository có thể có được các đối tượng
Product mà không cần phải biết gì về nơi nó đến hoặc làm thế nào mà các lớp thực thi sẽ chuyển nó đi. Đây là bản chất của repository
pattern. Tôi sẽ xem lại interface IProductRepository trong suốt quá trình phát triển để thêm vào các tính năng.
Bước 2. Thêm một constructor để khai báo một sự phụ thuộc trên interface IproductRepository trong mỗi controller, kèm theo 1
biến private

private IProductRepository repository;


public ProductController(IProductRepository productRepository)
{
this.repository = productRepository;
}

Bước 3. Viết 1 class định nghĩa tất cả các phương thức có trong interface

public class EFProductRepository : IProductRepository


{
private EFDbContext context = new EFDbContext();

public IEnumerable<Product> Products { get { return context.Products; } }


}
Trong đó EFDbContext là Entity Framework hoặc Framework khác dùng để kết nối với CSDL.
Thêm Pagination (Phân trang)
public ActionResult List(int page =1)//List chính hiển thị danh sách
{
return View(repository.Products
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize)
);
}
Trong đó:
- Skip((page-1)*PageSize) : Bỏ qua số mẫu tin theo công thức tính toán trong đó.
- Take(PageSize) : Lấy tổng số mẫu tin theo số PageSize

Thêm PagingInfo.cs
Để hiển thị link liên kết chuyển thông tin đến view về số lượng của các trang sẵn có, trang hiện tại, và tổng sản phẩm trong repository.
public class PagingInfo
{
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages
{
get
{
return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage);
}
}
}
Thêm phương thức HTML Helper
Bây giờ tôi có một view model, tôi có thể thực thi các phương thức HTML helper, mà tôi sẽ gọi PageLinks. Tạo một thư mục mới trong
project SportsStore.WebUI tên là HtmlHelpers và thêm một lớp mới gọi là PagingHelpers.cs.
public static class PagingHelpers
{
public static MvcHtmlString PageLinks(this HtmlHelper html, PagingInfo pagingInfo,
Func<int, string> pageUrl)
{
StringBuilder result = new StringBuilder();

for (int i = 1; i <= pagingInfo.TotalPages; i++)


{
TagBuilder tag = new TagBuilder("a");
tag.MergeAttribute("href", pageUrl(i));
tag.InnerHtml = i.ToString();
if (i == pagingInfo.CurrentPage)
{
tag.AddCssClass("selected");
tag.AddCssClass("btnprimary");
}
tag.AddCssClass("btn btndefault");
result.Append(tag.ToString());
}
return MvcHtmlString.Create(result.ToString());
}
}
Phương pháp mở rộng PageLinks tạo ra HTML cho một tập hợp các liên kết trang bằng cách sử dụng các thông tin được cung cấp trong một
đối tượng PagingInfo. Tham số Func chấp nhận một đại diện mà nó sử dụng để tạo ra các liên kết xem các trang khác.

Thêm Namespace cho phương thức HTML Helper vào file Views/web.config
<add namespace="SportsStore.WebUI.HtmlHelpers"/>
Thêm Dữ liệu View Model
Tôi chưa hoàn toàn sẵn sàng để sử dụng phương thức HTML helper. Tôi chưa cung cấp một thể hiện của lớp view model PagingInfo đến view.
Tôi có thể làm điều này bằng cách sử dụng tính năng view bag, nhưng tôi muốn bọc tất cả dữ liệu mà tôi sẽ gửi từ controller đến view trong
một lớp view model duy nhất. Để làm điều này, tôi đã thêm một file tên là ProductsListViewModel.cs vào thư mục Models của project
SportsStore.WebUI

Cập nhật phương thức List trong lớp ProductController


Sử dụng lớp ProductsListViewModel đề cung cấp view với các chi tiết của sản phẩm hiển thị trên trang và những chi tiết của phân trang.
public ActionResult List(int page =1)
{
ProductsListViewModel model = new ProductsListViewModel()
{
Products = repository.Products
.OrderBy(p=>p.ProductID)
.Skip((page-1)*PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo()
{
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = repository.Products.Count()
}
};

return View(model);
}
Cập nhật file List.cshtml
@model SportsStore.WebUI.Models.ProductsListViewModel

@{

ViewBag.Title = "Products";
}
@foreach (var p in Model.Products)
{
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}

Hiển thị Page Links (Liên kết trang)


Gọi phương thức HTML Helper trong file List.cshtml
<div>
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x }))
</div>
Trong đó:
Url.Action(“List”, new {page=x}) là phương thức hiển thị số trang. “List” là action, page là biến dùng
trong controller.

Cải thiện URLs


routes.MapRoute(
name: null,
url: "Trang{page}",
defaults: new { Controller = "Product", action = "List" }
);
Áp dụng Bootstrap CSS cho file _Layout.cshtml
Thêm link liên kết tới file CSS vào file _Layout.cshtml trong thẻ <head></head>

<link href="~/Content/bootstrap.css" rel="stylesheet" />


<link href="~/Content/bootstrap-theme.css" rel="stylesheet" />

Listing 8-8:Những Content của file Menu.cshtml


@model IEnumerable<string>

@Html.ActionLink("Trang chủ", "List", "Product", null, new { @class="btn btn-block btn-default btn-lg"})

@foreach(var link in Model)


{
@Html.RouteLink(link, new
{
controller="Product",
action="List",
category=link,
page=1
}
, new { @class="btn btn-block btn-default btn-lg"})
}
Xây dựng giỏ hàng

Nút Add to Cart sẽ được xuất hiện bên cạnh mỗi sản phẩm trong catalog. Nhấn vào nút này sẽ thể thiện
tổng số sản phẩm mà customer đã được lựa chọn cho đến thời điểm đó, bao gồm cả tổng tiền. Lúc này,
customer sẽ có thể nhấn vào nút Continue Shopping để quay trở lại trang danh mục sản phẩm, hoặc nhấn nút
Checkout Now để hoàn tất việc đặt hàng và kết thúc việc shopping.
Defining Cart Entity
public class Cart
{
private List<CartLine> lineCollection = new List<CartLine>();
/// <summary>
/// Thêm 1 sản phẩm vào Cart
/// </summary>
/// <param name="product">Thông tin sản phẩm</param>
/// <param name="quantity">Số lượng</param>
public void AddItem(Product product, int quantity)
{
//Trước khi thêm kiểm tra xem nó có trong giỏ hàng chưa
CartLine line = lineCollection.Where(p => p.Product.ProductID == product.ProductID)
.FirstOrDefault();
//Chưa có thì thêm
if(line==null)
{
lineCollection.Add(new CartLine { Product = product, Quantity = quantity });
}else//có rồi thì cộng dồn số lượng
{
line.Quantity += quantity;
}
}
/// <summary>
/// Xóa thông tin sản phẩm trong Cart
/// </summary>
/// <param name="product">Thông tin của sản phẩm cần xóa</param>
public void RemoveItem(Product product)
{
lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID);
}
/// <summary>
/// Tính tổng thành tiền
/// </summary>
/// <returns></returns>
public decimal ComputeTotalValue()
{
return lineCollection.Sum(p => p.Product.Price);
}
/// <summary>
/// Xóa toàn bộ thông tin giỏ hàng
/// </summary>
public void Clear()
{
lineCollection.Clear();
}
/// <summary>
/// Lấy toàn bộ thông tin của giỏ hàng
/// </summary>
public IEnumerable<CartLine> Lines
{
get
{
return lineCollection;
}
}
}
public class CartLine
{
public Product Product
{
get; set;
}
public int Quantity
{
get; set;
}
}
Class CartLine được sử dụng cho class Cart, được định nghĩa chung một tên, để đại diện cho sản phẩm đã được chọn bở customer và
lượng lớn người dùng muốn mua.

Adding the Add to Cart button


Ở đây tác giả cần chỉnh sửa lớp view Views/Shared/ProductSummary.cshtml để có thể thêm button vào trong danh
sách sản phẩm
@using (Html.BeginForm("AddToCart", "Cart"))
{
<div class="pull-right">
@Html.HiddenFor(x=>x.ProductID)
@Html.Hidden("returnURL",Request.Url.PathAndQuery)
<input type="submit" class="btn btn-success" value="AddToCart" />
</div>
}
Trong đó Html.BeginForm("AddToCart", "Cart") thì AddToCart là action, và Cart là controller.

Implementing the Cart Controller – Khai bào CartController


public class CartController : Controller
{
private IProductRepository repo;
public CartController(IProductRepository repository)
{
repo = repository;
}

public RedirectToRouteResult AddToCart(int productID,string returnURL)


{
Product product = repo.Products.FirstOrDefault(p => p.ProductID == productID);
if (product != null)
GetCart().AddItem(product, 1);

return RedirectToAction("Index", new { returnURL });

public RedirectToRouteResult RemoveFromCart(int productID, string returnURL)


{
Product product = repo.Products.FirstOrDefault(p => p.ProductID == productID);
if (product != null)
GetCart().RemoveItem(product);
return RedirectToAction("Index", new { returnURL });
}

private Cart GetCart()


{
Cart cart = (Cart)Session["Cart"];
if(cart==null)
{
cart = new Cart();
Session["Cart"] = cart;
}
return cart;
}
}

Có một vài điểm cần chú ý về Controller này.Đây là lần đầu tác giả sử dụng tính năng trạng thái phiên ASP.NET
đối với store và lấy lại đối tượng Cart.Đây là mục đích của phương thức GetCart.ASP.NET là một tính năng phiên đẹp
mà nó sử dụng cookies hoặc viết lại URL để liên kết nhiều thành phần yêu cầu chung từ người dùng tới tới hình thức
của một phiên truy cập. Những tính năng liên quan là trạng thái phiên mà nó liên kết dữ liệu với phiên. Đây là ý tưởng
hợp cho class Cart. Tác giả muốn mỗi người dùng sẽ có một giỏ hàng riêng, và mong muốn giỏ hàng sẽ tạo thành mối
liên kết với dữ liệu. Dữ liệu liên kết với phiên sẽ bị xoá khi một phiên hết hạn sử dụng ( Thường bởi vì một người
dùng sẽ không tạo yêu cầu trong một khoảng thời gian nào đó), điều đó có nghĩa là tác giả không cần quản lý kho hàng
hoặc vòng đời của đối tượng Cart . Để thêm đối tượng vào phiên trạng thái, tác giả đã thêm vào giá trị cho đối tượng
Session như sau:
Session["Cart"] = cart;
Để lấy lại đối tượng lần nữa. Chỉ cần đọc chung một khoá:
Cart cart = (Cart)Session["Cart"];
Hướng dẫn: đối tượng trạng thái phiên được lưu trữ ở bộ nhớ mặc đinh của ASP.NET server, nhưng bạn có
thể hiệu chỉnh lại các phương pháp lưu trữ, bao gồm cả việc dùng SQL database. Details : Pro ASP.NET MVC5
Platform.
Về phương thức Add to Cart, Remove from Cart, tác giả đã sử dụng tên thông số khớp với yếu tố
Input trong HTML form được tạo nằm trong ProductSummary.cshtml view. Điều này cho phép MVC
Framework liên kết những giá trị tới từ Post với những thông số này, nghĩa là không cần hiệu chỉnh form.
Displaying the Contents of the Cart
Điểm cuối cùng cần chú ý về controller Cart là cả hai phương thức Add to Cart và Remove from Cart
điều gọi phương thức RedirectToAction. Điều này sẽ ảnh hưởng tới việc gửi đi cấu trúc chuyển hướng HTTP tới
trình duyệt client, hỏi trình duyệt về yêu cầu một URL mới. Trong trường hợp này, tác giả đã yêu cầu trình duyệt
để gửi yêu cầu một URL mà sẽ gọi phương thức hoạt động Index của controller Cart.

Tạo ra một class mới tên là CartIndexViewModel.cs trong thư mục Models của project SportStore.WebUI.
public class CartIndexViewModel
{
public Cart Cart { get; set; }
public string RetrunUrl { get; set; }
}

Bây giờ ta đã có view model, ta có thể hoàn thành phương thức hoạt động Index trong class điều khiển
Cart.
public ActionResult Index(string returnUrl)
{
return View(new CartIndexViewModel { Cart = GetCart(), ReturnUrl = returnUrl });
}
Completing the Cart
Sử dụng Model Binding
Tôi tạo một mô hình Binder tuỳ chỉnh bằng triển khai interface System.Web.Mvc.IModelBinder. Để thực hiện, thêm một thư mục vào project
SportsStore.WebUI được gọi là Infrastructure/Binders và tạo một tập tin lớp CartModelBinder.cs
public class CartModelBinder : IModelBinder
{
private const string sessionKey = "Cart";

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)


{
//Lấy thông tin Caert từ Session
Cart cart = null;
if (controllerContext.HttpContext.Session!= null)
cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
if(cart==null)
{
cart = new Cart();
if (controllerContext.HttpContext.Session != null)
controllerContext.HttpContext.Session[sessionKey] = cart;
}
return cart;
}
}
Giao diện IModelBinder định nghĩa một phương thức: BindModel. Hai thông số được cung cấp để làm cho việc tạo ra các mô hình đối tượng
miền. Các ControllerContext cung cấp quyền truy cập vào tất cả các thông tin mà các lớp điều khiển có, trong đó bao gồm chi tiết về các yêu
cầu từ khách hàng. Các ModelBindingContext cung cấp cho bạn thông tin về các đối tượng mô hình bạn đang được yêu cầu xây dựng và một
số công cụ để làm cho quá trình liên kết dễ dàng hơn.
Đối với mục đích của tôi, lớp ControllerContext là thứ tôi đang quan tâm. Nó có tính năng HttpContext,mà lần lượt có một tính năng Session
cho phép tôi có được và thiết lập dữ liệu session. Tôi có thể có được các đối tượng Cart liên kết với session người dùng bằng cách đọc một giá
trị từ dữ liệu session, và tạo ra được một Cart nếu không có.
Khai báo với MVCFramework
Tôi cần phải nói cho MVCFramework rằng nó có thể sử dụng lớp CartModelBinder để tạo ra các trường của Cart. Tôi làm điều này trong các
phương pháp Application_Start của Global.asax
Thay đổi controller
Bổ sung thêm vào các phương thức trong controller 1 biến Cart cart và thay thế những lệnh có dùng đến GetCart() thành cart.
public ActionResult Index(Cart cart,string returnUrl)
{
return View(new CartIndexViewModel { Cart = cart, ReturnUrl = returnUrl });
}
Hoàn thành Cart
Removing Items from the Cart
Thêm dòng code sau vào Index.cshtml (view của controller Cart)

<td>
@using (Html.BeginForm("RemoveFromCart", "Cart"))
{
@Html.Hidden("productID", line.Product.ProductID)
@Html.HiddenFor(x=>x.ReturnUrl)
<input type="submit" class="btn btn-sm btn-warning" value="Xóa" />
}
</td>
Adding the Cart Summary
public PartialViewResult Summary(Cart cart)
{
return PartialView(cart);
}
Tạo thêm view
@model SportsStore.Domain.Entities.Cart
<div class="navbar-right">
@Html.ActionLink("Checkout", "Index", "Cart",
new { returnUrl = Request.Url.PathAndQuery },
new { @class = "btn btn-default navbar-btn" })
</div>

<div class="navbar-text navbar-right">


<b>Your cart:</b>
@Model.Lines.Sum(x => x.Quantity) item(s),
@Model.ComputeTotalValue().ToString("c")
</div>
Thêm 1 render của Summary vào _Layout.cshtml
<div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">SPORTS STORE</a>
@Html.Action("Summary","Cart")
</div>
Submitting Orders
Thêm một tập tin class gọi là ShippingDetails.cs
public class ShippingDetails
{
[Required(ErrorMessage = "Please enter a name")]
public string Name { get; set; }

[Required(ErrorMessage = "Please enter the first address line")]


public string Line1 { get; set; }

public string Line2 { get; set; }

public string Line3 { get; set; }

[Required(ErrorMessage = "Please enter a city name")]


public string City { get; set; }

[Required(ErrorMessage = "Please enter a state name")]


public string State { get; set; }
public string Zip { get; set; }

[Required(ErrorMessage = "Please enter a country name")]


public string Country { get; set; }

public bool GiftWrap { get; set; }


}

Adding the Checkout Process


Adding the Checkout Now Button to the Index.cshtml File
@Html.ActionLink("Thanh toán", "Checkout", null, new { @class="btn btn-primary"})

Thêm phương thức Checkout


public ViewResult Checkout()
{
return View(new ShippingDetails());
}

[HttpPost]
public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails)
{
if (cart.Lines.Count() == 0)
{
ModelState.AddModelError("", "Sorry, your cart is empty!");
}
if (ModelState.IsValid)
{
orderProcessor.ProcessOrder(cart, shippingDetails);
cart.Clear();
return View("Completed");
}
else
{
return View(shippingDetails);
}
}

Add view Checkout.cshtml


@model SportsStore.Domain.Entities.ShippingDetails

@{
ViewBag.Title = "SportStore: Checkout";
}
<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>
@using (Html.BeginForm())
{ @Html.ValidationSummary("Thông báo lỗi",htmlAttributes:new { @class="text-danger"})
<h3>Ship to</h3>
<div class="form-group">
<label>Name:</label>
@Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
</div>

<h3>Address</h3>
foreach (var property in ViewData.ModelMetadata.Properties)
{
if (property.PropertyName != "Name" && property.PropertyName != "GiftWrap")
{
<div class="form-group">
<label>@(property.DisplayName ?? property.PropertyName)</label>
@Html.TextBox(property.PropertyName, null, new { @class = "form-control" })
</div>
}
}
<h3>Options</h3>
<div class="checkbox">
<label>
@Html.EditorFor(x => x.GiftWrap) Gift wrap these items
</label>
</div>
<div class="text-center">
<input class="btn btn-primary" type="submit" value="Complete order" />
</div>
}

Định nghĩa Interface Order Processor


public interface IOrderProcessor
{
void ProcessOrder(Cart cart, ShippingDetails shippingDetails);
}

Bổ sung phương thức xử lý cho Interface IorderProcessor


CS File\EFEmailOrderProcessor.cs

Đăng ký vào kernel.Bind()


EmailSettings emailsetting = new EmailSettings()
{
WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false")
};
kernel.Bind<IOrderProcessor>().To<EFEmailOrderProcessor>().WithConstructorArgument("setting", emailsetting);

Thêm key Email.WriteAsFile vào Web.config ở mục appSettings


<add key="Email.WriteAsFile" value="true"/>

Sửa constructor của CartController thành


private IOrderProcessor orderProcessor;

public CartController(IProductRepository repository, IOrderProcessor order)


{
repo = repository;
orderProcessor = order;
}
Enabling Client-SideValidation Bật cơ chế xác thực bên phía Client
1. Cài đặt gói Microsoft.jQuery.Unobtrusive.Validation
2. Thêm các các liên kết đến các thư viện trong file Layout.cshtml
<script src="~/Scripts/jquery-1.9.1.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js"></script>

3. Bật tắt tính năng xác thực phía Client bằng câu lệnh trong từng VIEW muốn sử dụng
HtmlHelper.ClientValidationEnabled = false;
HtmlHelper.UnobtrusiveJavaScriptEnabled = false;

Chú ý: false là tắt, true là bật

Hoặc thiết lặp cho cả hệ thống bằng cách chỉnh sửa trong file Web.config
<appSettings>
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" /
</appSettings>

Bảo mật về Quyền quản trị


Cấu hình xác thực trong tập tin Web.config thẻ system.web
<authentication mode="Forms">
<forms loginUrl="/Account/Login" timeout="2880"/>
</authentication>

Xác định tên người dùng và mật khẩu trong tập tin Web.config
<authentication mode="Forms">
<forms loginUrl="/Account/Login" timeout="2880">
<credentials passwordFormat="Clear">
<user name="admin" password="024125"/>
</credentials>
</forms>
</authentication>
Thêm thuộc tính quyền sở hữu vào tập tin AdminControl.cs
Thêm thuộc tính [Authorize] vào đầu controller Admin

Tạo các nhà cung cấp xác thực


Sử dụng tính năng xác thực yêu cầu cuộc gọi đến hai phương pháp tĩnh của lớp System.Web.Security.FormsAuthentication:

 Các phương pháp xác nhận thông tin được cung cấp bởi người sử dụng.
 Phương pháp SetAuthCookie thêm một cookie để đáp ứng các trình duyệt, do đó người dùng không cần phải xác thực mỗi khi họ thực hiện một
yêu cầu.

Tạo một thư mục mới có tên gọi Abstract trong tập tin Infrastructure của dự án SportsStore.WebUI và thêm một giao diện mới gọi là IAuthProvider.
Nội dung của giao diện này:
namespace SportsStore.WebUI.Infrastructure.Abstract
{
public interface IAuthProvider
{
bool Authenticate(string username, string password, bool rememberMe = false);
}
}

Tạo một thực hiện của giao diện trên

namespace SportsStore.WebUI.Infrastructure.Concrete
{
public class FormsAuthProvider : Abstract.IAuthProvider
{
public bool Authenticate(string username, string password, bool rememberMe=false)
{
#pragma warning disable CS0618 // Type or member is obsolete
bool result = FormsAuthentication.Authenticate(username, password);
#pragma warning restore CS0618 // Type or member is obsolete
if (result)
FormsAuthentication.SetAuthCookie(username, rememberMe);
return result;
}
}
}

Tạo mối liên kết trong file NinjectDependencyResolver


kernel.Bind<IAuthProvider>().To<FormsAuthProvider>();

Tạo Bộ điều khiển tài khoản


public class LoginViewModel
{
[Required(ErrorMessage ="Điền tên đăng nhập")]
[DisplayName("Tên đăng nhập")]
public string UserName { get; set; }

[Required(ErrorMessage ="Điền mật khẩu")]


[DisplayName("Mật khẩu")]
public string Password { get; set; }

[DisplayName("Ghi nhớ")]
public bool RememberMe { get; set; }
}

Tạo controller
public class AccountController : Controller
{
private IAuthProvider account;

public AccountController(IAuthProvider acc)


{
account = acc;
}

public ViewResult Login()


{
return View();
}

[HttpPost]
public ActionResult Login(LoginViewModel model,string returnUrl)
{
if (ModelState.IsValid)
{
if (account.Authenticate(model.UserName, model.Password, model.RememberMe))
return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
else
{
ModelState.AddModelError("", "Tên đăng nhập hoặc tài khoản không đúng!");
return View();
}
}
return View();
}
}

Tạo view

Custom Redirect URL


Chỉnh sữa loginUrl thành URL mong muốn
<forms loginUrl="/DangNhap" timeout="2880">
<credentials passwordFormat="Clear">
<user name="admin" password="024125"/>
</credentials>
</forms>

Thêm dòng sau đây vào tập tin RouteConfig.cs


routes.MapMvcAttributeRoutes();

Thêm dòng sau đây vào đầu của controller muốn thay đổi redirect Url
[Route("DangNhap")]

Remove ?returnurl=%2f
Thêm bộ code sau vào tập tin Global.asax.cs
#region Custum Redirect Url
private const string ReturnUrlRegexPattern = @"\?ReturnUrl=.*$";
public MvcApplication()
{
PreSendRequestHeaders += MvcApplicationOnPreSendRequestHeaders;
}
private void MvcApplicationOnPreSendRequestHeaders(object sender, EventArgs e)
{

string redirectUrl = Response.RedirectLocation;

if (string.IsNullOrEmpty(redirectUrl)
|| !System.Text.RegularExpressions.Regex.IsMatch(redirectUrl, ReturnUrlRegexPattern))
{

return;

Response.RedirectLocation = System.Text.RegularExpressions.Regex.Replace(redirectUrl,
ReturnUrlRegexPattern,
string.Empty);
}
#endregion

Tạo modal
Chèn đoạn code sau vào dưới cùng của trang view
<script>
$(document).ready(function () {
$('#deleteProductModal').on('show.bs.modal', function (event) { // id of the modal with event
var button = $(event.relatedTarget) // Button that triggered the modal
var productid = button.data('productid') // Extract info from data-* attributes
var productname = button.data('productname')

var title = 'Confirm Delete #' + productid


var content = 'Are you sure want to delete ' + productname + '?'

// Update the modal's content.


var modal = $(this)
modal.find('.modal-title').text(title)
modal.find('.modal-body').text(content)

// And if you wish to pass the productid to modal's 'Yes' button for further processing
modal.find('button.btn-danger').val(productid)
})
})
</script>
Tạo 1 div giữ chỗ cho modal
<!-- Modal -->
<div class="modal fade" id="deleteProductModal" tabindex="-1" role="dialog" aria-labelledby="deleteProductModalLabel" aria-
hidden="true"> // thêm 1 option nữa để không thể bỏ qua modal bằng việc click chuột sang chỗ khác
//-------------------- data-backdrop="false" --------------------------//
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-
hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteProductModalLabel">Confirm Delete</h4>
</div>
<div class="modal-body">
Are you sure want to delete the product?
</div>
<div class="modal-footer">
@using (Html.BeginForm("Delete", "Admin", FormMethod.Post))
{ //Mặc định
<button name="ProductID" type="submit" class="btn btn-danger">
Yes
</button>

<button type="button" class="btn btn-default" data-dismiss="modal">


No
</button>
}
</div>
</div>
</div>
</div>
<!-- End Modal -->

Chèn nút gọi modal


<button type="button" class="btn btn-warning" data-toggle="modal"
data-target="#deleteProductModal" data-productid=@p.ProductID data-productname="@p.Name">
<span class="glyphicon glyphicon-remove"></span>
Delete
</button>
Tạo modal được gọi bằng function jquery
<!--Modal define-->
<div class="modal fade" id="deleteProductModal" tabindex="-1" role="dialog" aria-labelledby="deleteProductModalLabel" aria-
hidden="true">
<!--thêm 1 option nữa để không thể bỏ qua modal bằng việc click chuột sang chỗ khác
//-------------------- data-backdrop="false" --------------------------//-->
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header alert-danger">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-
hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteProductModalLabel">Confirm Delete</h4>
</div>
<div class="modal-body">
Are you sure want to delete the product?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
OK
</button>
</div>
</div>
</div>
</div>

<!--End modal define-->

<script>

function callModal(jQuery) {
var title = 'Thông báo lỗi'
var content = 'Bạn không thể block user này'

// Update the modal's content.


var modal = $('.modal-dialog')
modal.find('.modal-title').text(title)
modal.find('.modal-body').text(content)
$('#deleteProductModal').modal()
};

</script>
Gọi nó bằng cách dùng callModal();

Thay đổi tên của Action


Chỉ cần chèn thêm dòng [ActionName(“Tên thay đổi”)] trước các phương thức action

Gọi Ajax thay đổi giá trị không cần mở FORM


1. Đặt 1 class hoặc 1 id để phân biệt và gọi trong jQuery
2. Đặt 1 data-id = @item.id để phân biệt các dòng trong table với nhau
3. Tạo mới 1 file Script để viết function vào đó
var product = {
init: function () {
product.registerEvents();
},
registerEvents: function () {
$("a#btnChangeStatus").bind('click', function (e) {
e.preventDefault();
var btn = $(this);
var id = btn.data("id");
$.ajax({
url: "/Product/ChangeStatus",
data: { productid: id },
dataType: "json",
type: "POST",
success: function (response) {
if (response.status == true) {
btn.text('Active');
btn.css("color", "#337ab7");
}
else {
btn.text('Blocked');
btn.css("color", "red");
}
}
});
});
}
};
product.init()
Chèn List of all link in Website
Tạo Model
public class MenuInfo
{
public string Action { get; set; }
public string Area { get; set; }
public string Attributes { get; set; }
public string Controller { get; set; }
public string ReturnType { get; set; }
public string MenuLink { get; set; }
}
public class ListMenu
{
public string ParentMenu { get; set; }
public IEnumerable<MenuInfo> ChildMenu { get; set; }
}

Tạo controller tên GetMenuXml


public ActionResult GetMenuXml()
{
var projectName = Assembly.GetExecutingAssembly().FullName.Split(',')[0];

Assembly asm = Assembly.GetAssembly(typeof(MvcApplication));

var model = asm.GetTypes().


SelectMany(t => t.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
.Where(
d => d.ReturnType.Name == "ActionResult" &&
!d.IsDefined(typeof(NonActionAttribute)) &&
!d.IsDefined(typeof(HttpPostAttribute))
)
.Select(n => new MenuInfo()
{
Controller = n.DeclaringType?.Name.Replace("Controller", ""),
Action = n.Name,
ReturnType = n.ReturnType.Name,
Attributes = string.Join(",", n.GetCustomAttributes().Select(a => a.GetType().Name.Replace("Attribute", ""))),
Area = n.DeclaringType.Namespace.ToString().Replace(projectName + ".", "").Replace("Areas.",
"").Replace(".Controllers", "").Replace("Controllers", ""),
MenuLink = n.DeclaringType?.Name.Replace("Controller", "")+"/"+n.Name
});

var listMenu = model.GroupBy(g => g.Controller)


.Select(g => new ListMenu
{
ParentMenu = g.Key,
ChildMenu = g
});

return PartialView("GetMenuXml",listMenu.ToList());
}
Tạo Partial view có tên GetMenuXml

@{
Layout = null;

@model IEnumerable<SportsStore.Domain.Entities.ListMenu>

<div class="panel-group" id="accordion">


<div class="panel panel-default">

@foreach (var parent in Model)


{
<div class="panel-heading">
<h4>
<a data-toggle="collapse" data-parent="#accordion" href="#@parent.ParentMenu">
@parent.ParentMenu
</a>
</h4>
</div>

<div id="@parent.ParentMenu" class="panel-collapse collapse">


<div class="panel-body">
@foreach (var child in parent.ChildMenu)
{
<button value="@child.MenuLink" id="menuLink" class="list-group-item btn btn-default" >
@child.Action
</button>
}
</div>
</div>
}
</div>
</div>

<script>
$('button#menuLink').on('click', function (event) { // id of the modal with event
var button = $(this);
var value = button.val();
$("#textFind").val(value);

});

$('button#menuLink').on('dblclick', function (event) {


$("#MenuModal").modal("hide");
});
//function SetLink() {
// document.getElementById("textFind").value = GetLink();
//}
</script>
Tạo view Modal popup có tên _ModalGetMenu

<!--Button gọi modal xuất hiện-->


<button type="button" class="btn btn-warning" data-toggle="modal"
data-target="#MenuModal" data-productid="1" data-productname="1">
<span class="glyphicon glyphicon-camera"></span>
Menu
</button>

<div class="modal fade" data-backdrop="false" id="MenuModal" tabindex="-1" role="dialog" aria-labelledby="deleteProductModalLabel"


aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-
hidden="true">&times;</span></button>
<h4 class="modal-title" id="MenuModalLabel">Chọn Menu</h4>
</div>

<div class="modal-body">
<p class="warning">
<ul>
<li>
Nhấp chuột trái để chọn
</li>
<li>
Nhập đúp chuột để chọn và đóng
</li>
</ul>
</p>
@Html.Action("GetMenuXml", "GetAllLinkInApp")
</div>
</div>
</div>
</div>

<script>
$(document).ready(function () {
$('#MenuModal').on('show.bs.modal', function (event) { // id of the modal with event

})
})
</script>
Chèn dòng này vào trang _Layout
<!--Open modal to get menu link-->
@Html.Partial("_ModalGetMenu")
<!--End modal to get menu link-->
Nhúng CKFinder và CKEditor vào Website
1. Chép toàn bộ file trong CKFinder & CKEditor vào nơi nào đó trong Website
2. Mở file Config.ascx trong CKFinder và sửa thông tin sau
LicenseName = "TranMinhThuan";
LicenseKey = "ANYKPAVJ3CRKMYT2SJU22L9SSQYC1HEP";

BaseUrl = "/Content/DataUpload/"; //Nơi để Upload các tập tin

Mở file config.js trong CKEditor và thêm thông tin sau


config.syntaxhighlight_lang = 'csharp';
config.syntaxhighlight_hideControls = true;
config.langguage = 'vi';
config.filebrowserBrowseUrl = '../ckfinder/ckfinder.html';
config.filebrowserImageBrowseUrl = '../ckfinder/ckfinder.html?type=Images';
config.filebrowserFlashBrowseUrl = '../ckfinder/ckfinder.html?type=Flash';
config.filebrowserUploadUrl = '../ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Files';
config.filebrowserImageUploadUrl = '../ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Images';
config.filebrowserFlashUploadUrl = '../ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Flash';
CKFinder.setupCKEditor(null, '/Plugin/ckfinder');

3. Viết đoạn Script sau đây vào View cần nhúng CKFinder & CKEditor

<script>
$("#btnSelectImg").on("click", function (e) {
e.preventDefault();
var finder = new CKFinder();
finder.selectActionFunction = function (url) {
url = url.replace("%20", " ");
url = url.replace("%2C", ",");
$("#txtImage").val(url);
};
finder.popup();
});

var editor = CKEDITOR.replace('txtDescription',


{
customconfig: '~/Plugin/ckeditor/config.js">',
});
</script>

Chú thích: #btnSelectImg là nút sẽ mở CKFinder lên

#txtImage là text box sẽ nhận đường dẫn trả về của CKFinder

<button type="button" class="btn btn-primary" id="btnSelectImg" href="#">Upload hình</button>

@Html.TextBoxFor(m => m.ImagePath, new { @id = "txtImage", @class = "form-control" })

4. Để sử dụng được Ckeditor phải chuyển thành TextArea cho tất cả các textbox cần chuyển
5. Thêm attribute [ValidateInput(false)] vào action gọi view để không bị lỗi
6. Chuyển các textbox thành @Html.Raw(@Model.Description) để hiển thị được nội dung.

You might also like