《Pro ASP.NET MVC 3 Framework》學習筆記之十七【示例項目SportsStore】


接下來是關於SportsStore的后台管理功能,也就是通常的CRUD操作。
首先添加一個AdminController,代碼如下:

View Code
using System.Web.Mvc; 
using SportsStore.Domain.Abstract;

namespace SportsStore.WebUI.Controllers {

public class AdminController : Controller {
private IProductRepository repository;

public AdminController(IProductRepository repo) {
repository = repo;
}
}
}

我們通過一個List page來展示已有的products,接着在AdminController里面添加一個Action:

View Code
using System.Web.Mvc; 
using SportsStore.Domain.Abstract;

namespace SportsStore.WebUI.Controllers {

public class AdminController : Controller {
private IProductRepository repository;

public AdminController(IProductRepository repo) {
repository = repo;
}

public ViewResult Index() {
return View(repository.Products);
}
}
}

為我們的后台管理界面創建一個布局_AdminLayout.cshtml,在 Views/Shared上右鍵添加新項MVC 3 Layout Page

View Code
<!DOCTYPE html> 

<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Admin.css")" rel="stylesheet" type="text/css" />
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>

接着添加一個樣式表單Admin.css,如下所示:

View Code
BODY, TD { font-family: Segoe UI, Verdana } 
H1 { padding: .5em; padding-top: 0; font-weight: bold;
font-size: 1.5em; border-bottom: 2px solid gray; }
DIV#content { padding: .9em; }
TABLE.Grid TD, TABLE.Grid TH { border-bottom: 1px dotted gray; text-align:left; }
TABLE.Grid { border-collapse: collapse; width:100%; }
TABLE.Grid TH.NumericCol, Table.Grid TD.NumericCol {
text-align: right; padding-right: 1em; }
FORM {margin-bottom: 0px; }
DIV.Message { background: gray; color:White; padding: .2em; margin-top:.25em; }

.field-validation-error { color: red; display: block; }
.field-validation-valid { display: none; }
.input-validation-error { border: 1px solid red; background-color: #ffeeee; }
.validation-summary-errors { font-weight: bold; color: red; }
.validation-summary-valid { display: none; }

添加Index視圖:

View Code
@model IEnumerable<SportsStore.Domain.Entities.Product>
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h1>
All Products
</h1>
<table class="Grid">
<tr>
<th>
ID
</th>
<th>
Name
</th>
<td>
ShowImg
</td>
<th class="NumericCol">
Price
</th>
<th>
Actions
</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>@item.ProductID
</td>
<td>@Html.ActionLink(item.Name, "Edit", new { item.ProductID })
</td>
<td>
<img src="/UploadImgs/@item.ImageURL" alt="img" id="@item.ProductID" width="100" height="100" />
</td>
<td class="NumericCol">@item.Price.ToString("c")
</td>
<td>
@using (Html.BeginForm("Delete", "Admin"))
{
@Html.Hidden("ProductID", item.ProductID)
<input type="submit" value="Delete" />
}
</td>
</tr>
}
</table>
<p>@Html.ActionLink("Add a new product", "Create")</p>

這里對數據庫有改動,需要增加一個字段ImageURL(varchar(255)),並且在我們的SportsStore.WebUI項目新添加一個文件夾UploadImgs用來存放上傳的圖片。

編輯Product

創建一個Edit Action

View Code
public ViewResult Edit(int productId) { 
Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
return View(product);
}

創建Edit View,強類型的並引用了_AdminLayout.cshtml,

如下:

View Code
@model SportsStore.Domain.Entities.Product
@{
ViewBag.Title = "Admin:Edit" + @Model.Name;
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h2>
Edit @Model.Name</h2>
@using (Html.BeginForm("Edit", "Admin", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
@Html.EditorForModel()
<div class="editor-label">
Image</div>
<div class="editor-field">
@if (Model.ImageURL == null)
{
@:None
}
else
{
<img width="150" height="150" alt="img" src="/UploadImgs/@Model.ImageURL" />
}
<div>
Upload new image:<input type="file" name="Image" /></div>
</div>
<input type="submit" value="Save" />
@Html.ActionLink("Cancel and return to list", "Index");
}

如果運行程序,這里並不會顯示ProductID,因為這里沒有必要顯示出來。實現這個效果需要在Product實體里面使用模型元數據(model metedata)。它能讓我們應用Attributes到Product的屬性上面來影響Html.EditorForModel()方法輸出的布局。

如下:

View Code
    public class Product
{
//通過這個屬性來在隱藏ProductID
[HiddenInput(DisplayValue = false)]
public int ProductID { get; set; }

[Required(ErrorMessage = "Please enter a product name")]
public string Name { get; set; }

[Required(ErrorMessage = "Please enter a description")]
[DataType(DataType.MultilineText)]
public string Description { get; set; }

[Required]
[Range(typeof(decimal), "0.01", "999999999999999", ErrorMessage = "Please enter a positive price")]
public decimal Price { get; set; }

[Required(ErrorMessage = "Please specify a category")]
public string Category { get; set; }

public string ImageURL { get; set; }
}

HiddenInput特性讓MVC框架將該屬性作為隱藏表單元素呈現,DataType特性允許我們指定一個值怎樣顯示或編輯。如這里將Description顯示為MultilineText(多行文本)
這里需要添加一個引用System.Web.Mvc。

接着添加一些樣式到Admin.css里面,如下:

View Code
.editor-field { margin-bottom: .8em; } 
.editor-label { font-weight: bold; }
.editor-label:after { content: ":" }
.text-box { width: 25em; }
.multi-line { height: 5em; font-family: Segoe UI, Verdana; }

這里看發現css里面的類名如.editor-field,我們並沒有在View里面定義一個class屬性,這個是MVC框架生成的,你可以查看頁面源文件。而且ID都是屬性名,這是模型綁定的結果,在前面的筆記有這些內容的。

為了編輯Product,我們需要添加相應的Repository和接口。在IProductsRepository里面添加一個方法:void SaveProduct(Product product);
接着實現該接口,如下:

View Code
    public class ProductsRepository : IProductsRepository
{
ISqlMapper mapper = MapperHelper.Instance();
public IList<Product> Products
{
get
{

return mapper.QueryForList<Product>("Product-Select", "");
}
}
public void SaveProduct(Product product)
{
mapper.Update("Product-Update", product);
}

public void AddProduct(Product product)
{
mapper.Insert("Product-Insert", product);
}


public void DeleteProduct(Product product)
{
mapper.Delete("Product-Delete", product);
}
}

初次路過的朋友可能會驚訝這里是什么東東,書中用的EF框架+MS SQL Server。我學着用的是iBatisnet+MySQL。如果你對iBatisnet不了解,可以先用下iBatisnet,上手還算比較容易。你也可以看看我前面筆記之十二,里面講了我這里的使用情況。

這里我們需要在iBatisnet的映射文件Product.xml添加update語句,如下:

View Code
<?xml version="1.0" encoding="utf-8" ?>
<sqlMap namespace="Product"
xmlns="http://ibatis.apache.org/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >
<alias>
<typeAlias alias="Product" type="SportsStore.Domain.Entities.Product,SportsStore.Domain"></typeAlias>
</alias>

<resultMaps>
<resultMap id="Product-Result" class="Product">
<result property="ProductID" column="ProductID"/>
<result property="Name" column="Name"/>
<result property="Description" column="Description"/>
<result property="Category" column="Category"/>
<result property="Price" column="Price" nullValue="0.00" dbType="decimal"/>
<result property="ImageURL" column="ImageURL" nullValue=""/>
</resultMap>
</resultMaps>

<statements>
<select id="Product-Select" parameterClass="Product" resultMap="Product-Result">
select * from products
</select>

<insert id="Product-Insert" parameterClass="Product">
insert into products(Name,Description,Category,Price,ImageURL) values
(
#Name#,#Description#,#Category#,#Price#,#ImageURL#
)
<selectKey resultClass="int" property="ProductID" type="post">
select @@IDENTITY
</selectKey>
</insert>

<update id="Product-Update" parameterClass="Product">
update products set Name=#Name#,Description=#Description#,Category=#Category#,Price=#Price#,ImageURL=#ImageURL#
where
ProductID=#ProductID#
</update>

<delete id="Product-Delete" parameterClass="Product">
delete from products where ProductID=#ProductID#
</delete>
</statements>

</sqlMap>

接着在AdminController里面重載一個處理HTTP Edit請求的action方法,如下:

        [HttpPost]
public ActionResult Edit(Product product, HttpPostedFileBase image)//HttpPostedFileBase提供了對上傳的文件單獨訪問
{
if (ModelState.IsValid)//如果驗證通過才執行編輯
{
if (image != null)
{
string path = @"D:\Study\Projects\SportsStore\SportsStore.WebUI\UploadImgs\" + image.FileName;
image.SaveAs(path);//保存圖片
product.ImageURL = image.FileName;

}
//接着執行保存的方法
repository.SaveProduct(product);
TempData["message"] = string.Format("{0} has been saved", product.Name);
return RedirectToAction("Index");
}
else
{
//如果驗證未通過,則繼續顯示編輯頁面,重新編輯
//there is something wrong with the data values
return View(product);
}
}

當驗證通過,會執行保存。這里我使用了TempData存儲了一條提示信息,TempData是MVC框架提供的一個非常爽的功能。它是一個Dictionary的鍵值對,類似於Session和ViewBag。有一個關鍵的不同點是TempData會在HTTP請求結束被刪除。

從上面的代碼我們注意下Edit方法的返回值類型ActionResult.前面我使用大多是ViewResult。ViewResult是從ActionResult派生的,當我們想呈現一個View時就可以使用ViewResult的返回值類型。當然,ActionResult的其他類型也是可用的,其中一個就是RedirectToAction方法的返回值類型。

這里我們不能使用ViewBag,因為用戶被重定向了,ViewBag在Controller和View傳遞數據並且ViewBag保存數據的時間不會比當前的Http請求,也就是說可能在Http請求沒有結束時ViewBag里面數據已經丟失了,這就達不到我們要的效果。當然Session可以保存,但是Session會一直保存(除非服務器內存不足或是過期),直到我們顯示的移除它,當然我們不情願這樣做。MVC框架提供的TempData功能就很完美的符合這里顯示提示信息的要求。而且TempData里面的數據是限制在單個用戶會話里面的(用戶之前是不能相互查看對方的TempData),並且會一直保存知道我們讀取了以后才被刪除。我們將會在使用重定向到用戶的action方法呈現View時讀取TempData的數據。

接着我們在_AdminLayout里面使用TempData,為的是能夠在引入了_AdminLayout的所以View里面使用。修改_AdminLayout.cshtml的代碼如下:

View Code
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Admin.css")" rel="Stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<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>
</head>
<body>
<div>
@if (TempData["message"] != null)
{
<div class="Message">@TempData["message"]</div>
}
@RenderBody()
</div>
</body>
</html>

上面我們引入了幾個js文件是用來進行客戶端驗證的,這又是MVC框架提供給我們的一個非常爽的功能,我只需要在Product.cs里面為屬性添加我需要限制的Attributes就能夠實現驗證用戶輸入的目的,引入了上面的兩個js文件后,默認是開啟了客戶端驗證的。當然服務器端驗證是一定會有的。在大多數情況下,我們都希望進行客戶端驗證,因為這樣會有一個比較好的用戶體驗。

但是如果我們不想進行客戶端驗證則可以這樣做,則可以使用這兩句代碼:

HtmlHelper.ClientValidationEnabled = false;
HtmlHelper.UnobtrusiveJavaScriptEnabled = false;

如果我們將這兩句代碼放到一個View或Controller里面,那客戶端驗證對當前的Action就不會使用。

我們也可以如果在配置文件里面配置來禁止整個程序使用客戶端驗證,如下配置:

View Code
<configuration> 
<appSettings>
<add key="ClientValidationEnabled" value="false"/>
<add key="UnobtrusiveJavaScriptEnabled" value="false"/>
</appSettings>
</configuration>

接着我們實現創建一個新的Product,添加兩個Create() action方法,一個是Get請求(沒有加HttpGet Attribute表示默認Get),一個是處理HttpGet請求。我們呈現添加頁面的時候調用的是Get,提交時調用Post。在AdminController里面添加的代碼如下:

View Code
        public ViewResult Create()
{
return View(new Product());
}

[HttpPost]
public ActionResult Create(Product product)
{
if (ModelState.IsValid)
{
repository.AddProduct(product);
TempData["message"] = string.Format("{0} has been added", product.Name);
return RedirectToAction("Index");
}
else
{
return View(product);
}
}

接着右鍵添加Create.cshtml,代碼如下:

View Code
@model SportsStore.Domain.Entities.Product
@{
ViewBag.Title = "Create";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h2>
Create</h2>
@using (Html.BeginForm())
{
@Html.EditorForModel()
<input type="submit" value="Add" />
}

實現刪除Product操作

首先添加一個接口方法:void DeleteProduct(Product product);
接着實現該方法

View Code
    public class ProductsRepository : IProductsRepository
{
ISqlMapper mapper = MapperHelper.Instance();
public IList<Product> Products
{
get
{

return mapper.QueryForList<Product>("Product-Select", "");
}
}
public void SaveProduct(Product product)
{
mapper.Update("Product-Update", product);
}

public void AddProduct(Product product)
{
mapper.Insert("Product-Insert", product);
}


public void DeleteProduct(Product product)
{
mapper.Delete("Product-Delete", product);
}
}

關於iBatisnet的映射文件,在上面已經全部列出來了,所以這里就不在展示了。接着是創建Delete action方法,如下:

View Code
        [HttpPost]
public ActionResult Delete(int productId)
{
Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (prod != null)
{
repository.DeleteProduct(prod);
TempData["message"] = string.Format("{0} was deleted", prod.Name);
}
return RedirectToAction("Index");
}

 

接着實現后台管理的安全性設置

asp.net mvc是建立在asp.net核心平台上的,所以我們可以訪問asp.net form驗證的功能,這也是一個比較通用的功能。只有登錄成功的人才能操作后台。關於From驗證,在書的22章會詳細講解。這里只是介紹了最基本的配置。在配置文件里面添加如下代碼:

View Code
<authentication mode="Forms"> 
<forms loginUrl="~/Account/LogOn" timeout="2880">
<credentials passwordFormat="Clear">
<user name="admin" password="secret" />
</credentials>
</forms>
</authentication>
<!--這里的timeout是2880分鍾即48小時,這里我們硬編碼用戶名和密碼,實際情況肯定不是這樣。-->

MVC框架有一個非常強大的功能稱為filters。這些都是Attributes,應用到一個action方法或Controller類。當一個請求被處理的時候,這些Attributes引入了額外的邏輯。我們也可以定義自己的filter。

這部分內容會在13章詳細介紹的,這里大概知道就行。其實我理解這里就是,filter功能用來實現權限控制,哪些action是需要權限的,只允許哪些用戶方法的等等。當添加了這些filter后,處理請求的時候就會做額外的判斷。

我們這里只是用到了Authorize。我們在AdminController類的上面添加這個filter,如下所示:

View Code
using System.Web.Mvc; 
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Linq;

namespace SportsStore.WebUI.Controllers {

[Authorize]
public class AdminController : Controller {
private IProductRepository repository;

public AdminController(IProductRepository repo) {
repository = repo;
}
...

這樣就表示只有通過驗證的才可能訪問該Controller,當然我可以細化到針對一個action方法。

這個時候運行程序,通過/Admin/Index URL進行后台會報錯,因為並不存在Account/LogOn.

下面我們開始實現登錄驗證功能,在Infrastructure文件夾里面再創建一個Abstract文件夾,在該文件夾里面添加一個接口IAuthProvider,代碼如下:

View Code
namespace SportsStore.WebUI.Infrastructure.Abstract { 
public interface IAuthProvider {

bool Authenticate(string username, string password);
}
}

接着在Infrastructure文件夾里面再創建一個Concrete文件夾,添加一個實現類FormsAuthProvider.如下:

View Code
using System.Web.Security; 
using SportsStore.WebUI.Infrastructure.Abstract;

namespace SportsStore.WebUI.Infrastructure.Concrete {

public class FormsAuthProvider : IAuthProvider {

public bool Authenticate(string username, string password) {

bool result = FormsAuthentication.Authenticate(username, password);
if (result) {
FormsAuthentication.SetAuthCookie(username, false);
}
return result;
}
}
}

然后使用Ninject添加綁定,如下:

        private void AddBindings()
{
ninjectKernel.Bind<IProductsRepository>().To<ProductsRepository>();
EmailSettings emailSettings = new EmailSettings {
 WriteAsFile = bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false") };
ninjectKernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings);
ninjectKernel.Bind<IAuthProvider>().To<FormsAuthProvider>();
}

接着創建一個AccountController和LogOn action方法,實際上是重載的兩個LogOn方法,一個處理Get呈現,一個處理Post提交驗證。這之前我們需要創建一個view model來傳遞。在SportsStore.WebUI/Models里面添加一個新的類LogOnViewModel,代碼如下:

View Code
using System.ComponentModel.DataAnnotations; 

namespace SportsStore.WebUI.Models {

public class LogOnViewModel {
[Required]
public string UserName { get; set; }

[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
}

接着我們添加AccountController,如下:

View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.WebUI.Infrastructure.Abstract;
using SportsStore.WebUI.Models;

namespace SportsStore.WebUI.Controllers
{
public class AccountController : Controller
{
private IAuthProvider authProvider;
public AccountController(IAuthProvider auth)
{
authProvider = auth;
}
public ViewResult LogOn()
{
return View();
}
[HttpPost]
public ActionResult LogOn(LogOnViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (authProvider.Authentication(model.UserName, model.Password))
{
return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
}
else
{
ModelState.AddModelError("", "Incorrect username or password");
return View();
}
}
else
{
return View();
}

}

}
}

右鍵添加LogOn View,如下:

View Code
@model SportsStore.WebUI.Models.LogOnViewModel

@{
ViewBag.Title = "LogOn";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}

<h2>Log In</h2>
<p>Please log in to access the administrator area:</p>
@using (Html.BeginForm())
{
@Html.ValidationSummary(true)
@Html.EditorForModel()
<p><input type="submit" value="Log in" /></p>
}

到這里,整個項目就結束了,呵呵!通過這個項目對MVC的理解應該加深了,后面進入到第二部分MVC3框架的講解。因為項目后面對數據庫進行了更改,所以你需要自己在相關的View里面添加顯示圖片的代碼。如果你是按照我的筆記實際操作,遇到任何問題請及時留言!

好了,今天的筆記就到這里,祝大家周末愉快!公司如果這幾天有年會的,祝大家都有好運中獎!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM