實做篇-將現有網站改成MVC架構
先前參加了佛心Jed哥的贈書活動,小弟也獲得了保哥的新書 [ASP.NET MVC2 開發實戰]一書,書中小弟獲益良多,最近也試著將現有的專案改以MVC的架構,在實做這個改變的過程,小弟便將其記錄下來並分享給各位。
一般來說,ASP.NET的網頁應用程式不外乎會有商業邏輯層(Business Tier)、資料層 (Data Tier)、網頁 (ASPX),而在網站的架構中我們可能習慣使用不同的NameSpace (資料夾)來區分商業邏輯層與資料層,基於商業機密公司應用程式不方便貼上來,小弟以Northwind資料庫實做一個範例來展現這個典型的網站架構。
這是一個客戶資料查詢的網頁程式,它是個簡單List [清單]類型的查詢的網頁程式,它有兩個查詢條件,1.客戶公司名稱、2.所在城市。查詢設計畫面如下:
查詢會呼叫CustomerBiz的商業邏輯,此為與Customer相關之商業邏輯層,SQL Statement會撰寫於此,通常查詢會是如下程式:
CustomerBiz.cs的完整程式碼如下所示,這裡Demo的範例很簡單,對應的舊兩個邏輯方法:
這裡筆者偷懶一下,使用組字串的方式,重點在於改以MVC的實作經驗的部分。而DAL的部分,由於擷取筆者專案實做的部分,包含了ExecuteSqlTran()、ExecuteScaler()、Query()、RunProcedure()等,非常攏長,因此筆者還是只擷取使用到的部分,也就是Query()的部分,程式碼如下:
原始架構的說明先到此,不過在這裡筆者想做一些不一樣的,要先說明一下,就是一般MVC的架構通常以Entity Framework,.NET的MVC的Model是直接的支援這樣的架構,於是突發奇想,如果我不想換掉DAL,該怎麼辦呢?因為我不想換掉已寫好的部分,假設有這樣的開發上的需求(有時在實際資訊環境,假想的情況無所不在!這是筆者的感覺),於是試想,不用Entity Framework,還是要有Model供View參考才好辦事吧,於是想到了利用Domain Service Class,快速的幫我們建立一個Customer的框架程式碼。不過當然,還是先建立一個MvcApplication專案吧。並建立個CustomerBiz,將CustomerBiz.cs、Customer.cs、DAL.cs複製到此資料夾中,如下:
對了,一個地方忘了說明,其中Customer.cs的Model是筆者利用RIA Service的 Domain Service Class 所建立出來的,筆者之後再將它複製過來而已。不過還是需要稍做修改並打上資料標籤,修改後Customer.cs的程式碼如下:
現在則要開始撰寫CustomerController.cs以便產生Customer的View了,不過要注意一點,筆者現在要做的是查詢的畫面,這個畫面並不是一開始進入就要秀出結果,畫面有兩個條件,[客戶公司名] 與 [所在位置],條件必然是透過Html Form的Submit方式將條件資料POST出去,接著才將查詢結果傳回以Grid的方式顯示出來。這個畫面在Web Form當然很好設計,但到了這裡我們就必須思考一下MVC的做法會是怎麼樣。
首先加入CustomerController.cs的 Index() 的一個空的方法,程式碼如下:
然後要產生對應的View就在這個方法中間點選滑鼠右鍵,點選 [加入檢視],注意!一定要在這個方法的中間點選滑鼠右鍵才會出現[加入檢視]的選項,否則是看不見的,因為在這裡IDE工具是有特殊的支援的,不信的話您可以在其他地方選右鍵,一定看不倒此選項。
在加入View之前請在Views的資料夾下面先建立一個Customer資料夾,因為我們希望Customer的部分就放置於此,基本上控制就是模組的名稱加上Controller結尾,這個是規定,所以View的名稱也是CustomerController去掉Controller",因此為”Customer”。
那麼點選[加入檢視]後會出現如下對話框,請選擇建立強行別的檢視,並選擇我們剛才建立的Customer 的Model,並要使用List的方式,才像我們在Web Form設計畫面要的結果,如下:
這個動作會對View資料夾下面的Customer資料夾加入一個Index.aspx檔案,這麼檔案的內容會是如下:
設計畫面會長這個樣子,畫面如下:
但目前這還是不是我要的,因為上面並沒有Form,也沒有[客戶公司名] 與 [所在位置]這兩個查詢條件,因此稍做修改,翻一下保哥的書了解MVC的Form可使用 using (Html.BeginForm()) 的方式宣告,如下:
查詢條件的部分,當然就使用<%=Html.TextBox("txtSearch") %>與 <%=Html.DropDownList("ddlCity"> 的描述語法來完成。這裡我們還要注意一點,Submit之後才要將資料帶回來,而Submit是HTTP的POST動作,這裡MVC也提供了一個Attribute 如下:
可讓我們將此接收POST的Controller部分拆開來寫,以免在同一個Controller方法中有過多複雜的判斷。還有,前面提到,為了使強行別的檢視也能夠接受既有DAL回傳的DataTable物件,因此筆者另外寫了一個QueryModelHelper<要轉換的Model>.GetModel(Model [要轉換的Model物件], DataTable [資料表])方法,筆者最後再列出此方法的內容。這時筆者撰寫完畢的CustomerController.cs程式碼如下:
各位應該有發現程式中多了一個GetCompanyCityModel()方法,那是筆者為了產生[所在程式]的下拉選單另外撰寫的,同時也共用了QueryModelHelper,QueryModelHelper的類別的程式碼如下:
這時程式已經可以執行,執行結果如下:
接著點選查詢,結果如下:
參考資料:
保哥的ASP.NET MVC 2開發實戰
ASP.Net MVC的 - 下拉列表相關聯的對象
http://zh-tw.w3support.net/index.php?db=so&id=627447
一般來說,ASP.NET的網頁應用程式不外乎會有商業邏輯層(Business Tier)、資料層 (Data Tier)、網頁 (ASPX),而在網站的架構中我們可能習慣使用不同的NameSpace (資料夾)來區分商業邏輯層與資料層,基於商業機密公司應用程式不方便貼上來,小弟以Northwind資料庫實做一個範例來展現這個典型的網站架構。
這是一個客戶資料查詢的網頁程式,它是個簡單List [清單]類型的查詢的網頁程式,它有兩個查詢條件,1.客戶公司名稱、2.所在城市。查詢設計畫面如下:
查詢會呼叫CustomerBiz的商業邏輯,此為與Customer相關之商業邏輯層,SQL Statement會撰寫於此,通常查詢會是如下程式:
1: protected void lbtSearch_Click(object sender, EventArgs e)
2: {
3: GetDatas();
4: }
5:
6: private void GetDatas()
7: {
8: CustomerBiz CBiz = new CustomerBiz();
9: DataView dv = new DataView(CBiz.GetCustomersByID_City(Request.Form["txtSearch"], Request.Form["ddlCity"]));
10: gvwList.DataSource = dv;
11: gvwList.DataBind();
12: }
CustomerBiz.cs的完整程式碼如下所示,這裡Demo的範例很簡單,對應的舊兩個邏輯方法:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using CustomerBiz.Data;
6: using System.Data;
7: using System.Data.SqlClient;
8:
9: namespace CustomerBiz
10: {
11: /// <summary>
12: /// CustomerBiz 的摘要描述
13: /// </summary>
14: public class eCustomerBiz: DAL
15: {
16: public DataTable GetCompanyCity()
17: {
18: string SqlStatement = @";
19: select 0 AS [ID], '' AS CITY
20: UNION
21: select DISTINCT ROW_NUMBER() OVER(ORDER BY City) AS [ID], C.city AS CITY from Customers C
22: Order by City"
23: try
24: {
25: DataTable dtResult = Query(SqlStatement).Tables[0];
26: return dtResult;
27: }
28: catch (Exception ex)
29: {
30: throw ex;
31: }
32: }
33:
34: public DataTable GetCustomersByID_City(
35: string customerId,
36: string city)
37: {
38: string SqlStatement = @";
39: SELECT CustomerID, CompanyName, ContactName, ContactTitle, Address, City, Region, PostalCode, Country, Phone, Fax
40: FROM Customers
41: WHERE CustomerID like @CustomerID
42: AND City=@City
43: "
44: try
45: {
46: SqlParameter [] paramCus = new SqlParameter[]
47: {
48: new SqlParameter("@CustomerID", SqlDbType.VarChar),
49: new SqlParameter("@City", SqlDbType.VarChar)
50: };
51:
52: paramCus[0].Value = "%"+customerId+"%";
53: if (city.Trim() != "")
54: paramCus[1].Value = city;
55: else
56: paramCus[1].Value = "%";
57:
58: return Query(SqlStatement, paramCus).Tables[0];
59: }
60: catch (Exception ex)
61: {
62: throw ex;
63: }
64: }
65: public eCustomerBiz()
66: {
67: //
68: // TODO: 在此加入建構函式的程式碼
69: //
70: }
71: }
72: }
這裡筆者偷懶一下,使用組字串的方式,重點在於改以MVC的實作經驗的部分。而DAL的部分,由於擷取筆者專案實做的部分,包含了ExecuteSqlTran()、ExecuteScaler()、Query()、RunProcedure()等,非常攏長,因此筆者還是只擷取使用到的部分,也就是Query()的部分,程式碼如下:
1: using System;
2: using System.Collections;
3: using System.Collections.Specialized;
4: using System.Data;
5: using System.Data.SqlClient;
6: using System.Configuration;
7:
8: namespace CustomerBiz.Data
9: {
10: /// <summary>
11: /// 建立日期:2007/10/26. by Gelis.
12: /// 資料訪問基礎類(基於SQLServer)
13: /// 用戶可以修改滿足自己專案的需要。
14: /// </summary>
15: public class DAL
16: {
17: //資料庫連接字串(使用 web.config來配置
18: protected static string connectionString = "";
19: protected SqlConnection rd_conn;
20: protected SqlDataReader myReader;
21:
22: public DAL()
23: {
24: connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["ConnStr_E"].ConnectionString;
25: }
26: private void PrepareCommand(SqlCommand cmd, SqlConnection conn, SqlTransaction trans, string cmdText, SqlParameter[] cmdParms)
27: {
28: if (conn.State != ConnectionState.Open)
29: conn.Open();
30: cmd.Connection = conn;
31: cmd.CommandText = cmdText;
32: if (trans != null)
33: cmd.Transaction = trans;
34: cmd.CommandType = CommandType.Text;//cmdType;
35: if (cmdParms != null)
36: {
37: foreach (SqlParameter parm in cmdParms)
38: cmd.Parameters.Add(parm);
39: }
40: }
41: /// <summary>
42: /// 執行SQL Statement
43: /// </summary>
44: /// <param name="SQLString">SQL Statement</param>
45: /// <returns>DataSet</returns>
46: public DataSet Query(string SQLString, params SqlParameter[] cmdParms)
47: {
48: using (SqlConnection connection = new SqlConnection(connectionString))
49: {
50: SqlCommand cmd = new SqlCommand();
51: PrepareCommand(cmd, connection, null, SQLString, cmdParms);
52: using (SqlDataAdapter da = new SqlDataAdapter(cmd))
53: {
54: DataSet ds = new DataSet();
55: try
56: {
57: da.Fill(ds, "ds");
58: cmd.Parameters.Clear();
59: return ds;
60: }
61: catch (System.Data.SqlClient.SqlException ex)
62: {
63: throw new Exception(ex.Message);
64: }
65: finally
66: {
67: if (connection.State != ConnectionState.Closed)
68: connection.Close();
69: connection.Dispose();
70: }
71: }
72: }
73: }
74: }
75: }
原始架構的說明先到此,不過在這裡筆者想做一些不一樣的,要先說明一下,就是一般MVC的架構通常以Entity Framework,.NET的MVC的Model是直接的支援這樣的架構,於是突發奇想,如果我不想換掉DAL,該怎麼辦呢?因為我不想換掉已寫好的部分,假設有這樣的開發上的需求(有時在實際資訊環境,假想的情況無所不在!這是筆者的感覺),於是試想,不用Entity Framework,還是要有Model供View參考才好辦事吧,於是想到了利用Domain Service Class,快速的幫我們建立一個Customer的框架程式碼。不過當然,還是先建立一個MvcApplication專案吧。並建立個CustomerBiz,將CustomerBiz.cs、Customer.cs、DAL.cs複製到此資料夾中,如下:
對了,一個地方忘了說明,其中Customer.cs的Model是筆者利用RIA Service的 Domain Service Class 所建立出來的,筆者之後再將它複製過來而已。不過還是需要稍做修改並打上資料標籤,修改後Customer.cs的程式碼如下:
1:
2: namespace CustomerBiz.Model
3: {
4: using System;
5: using System.Collections.Generic;
6: using System.ComponentModel;
7: using System.ComponentModel.DataAnnotations;
8: using System.Linq;
9: //using System.ServiceModel.DomainServices.Hosting;
10: //using System.ServiceModel.DomainServices.Server;
11:
12:
13: // The MetadataTypeAttribute identifies CustomersMetadata as the class
14: // that carries additional metadata for the Customers class.
15: [MetadataTypeAttribute(typeof(Customers))]
16: public partial class Customers
17: {
18: // Metadata classes are not meant to be instantiated.
19: public Customers()
20: {
21: }
22:
23: [DisplayName("地址")]
24: public string Address { get; set; }
25:
26: [DisplayName("城市")]
27: public string City { get; set; }
28:
29: [DisplayName("公司名稱")]
30: public string CompanyName { get; set; }
31:
32: [DisplayName("聯絡人名稱")]
33: public string ContactName { get; set; }
34:
35: [DisplayName("聯絡人職稱")]
36: public string ContactTitle { get; set; }
37:
38: [DisplayName("國家")]
39: public string Country { get; set; }
40: [Required]
41: [DisplayName("客戶代碼")]
42: public string CustomerID { get; set; }
43:
44: [DisplayName("傳真")]
45: public string Fax { get; set; }
46:
47: [DisplayName("電話")]
48: public string Phone { get; set; }
49:
50: [DisplayName("郵遞區號")]
51: public string PostalCode { get; set; }
52:
53: [DisplayName("地區")]
54: public string Region { get; set; }
55:
56: }
57: }
現在則要開始撰寫CustomerController.cs以便產生Customer的View了,不過要注意一點,筆者現在要做的是查詢的畫面,這個畫面並不是一開始進入就要秀出結果,畫面有兩個條件,[客戶公司名] 與 [所在位置],條件必然是透過Html Form的Submit方式將條件資料POST出去,接著才將查詢結果傳回以Grid的方式顯示出來。這個畫面在Web Form當然很好設計,但到了這裡我們就必須思考一下MVC的做法會是怎麼樣。
首先加入CustomerController.cs的 Index() 的一個空的方法,程式碼如下:
1: public ActionResult Index()
2: {
3:
4: }
在加入View之前請在Views的資料夾下面先建立一個Customer資料夾,因為我們希望Customer的部分就放置於此,基本上控制就是模組的名稱加上Controller結尾,這個是規定,所以View的名稱也是CustomerController去掉Controller",因此為”Customer”。
那麼點選[加入檢視]後會出現如下對話框,請選擇建立強行別的檢視,並選擇我們剛才建立的Customer 的Model,並要使用List的方式,才像我們在Web Form設計畫面要的結果,如下:
這個動作會對View資料夾下面的Customer資料夾加入一個Index.aspx檔案,這麼檔案的內容會是如下:
1: <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<CustomerBiz.Model.Customers>>" %>
2:
3: <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
4: Index
5: </asp:Content>
6:
7: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
8:
9: <h2>Index</h2>
10:
11: <table>
12: <tr>
13: <th></th>
14: <th>
15: Address
16: </th>
17: <th>
18: City
19: </th>
20: <th>
21: CompanyName
22: </th>
23: <th>
24: ContactName
25: </th>
26: <th>
27: ContactTitle
28: </th>
29: <th>
30: Country
31: </th>
32: <th>
33: CustomerID
34: </th>
35: <th>
36: Fax
37: </th>
38: <th>
39: Phone
40: </th>
41: <th>
42: PostalCode
43: </th>
44: <th>
45: Region
46: </th>
47: </tr>
48:
49: <% foreach (var item in Model) { %>
50:
51: <tr>
52: <td>
53: <%: Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) %> |
54: <%: Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ })%> |
55: <%: Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })%>
56: </td>
57: <td>
58: <%: item.Address %>
59: </td>
60: <td>
61: <%: item.City %>
62: </td>
63: <td>
64: <%: item.CompanyName %>
65: </td>
66: <td>
67: <%: item.ContactName %>
68: </td>
69: <td>
70: <%: item.ContactTitle %>
71: </td>
72: <td>
73: <%: item.Country %>
74: </td>
75: <td>
76: <%: item.CustomerID %>
77: </td>
78: <td>
79: <%: item.Fax %>
80: </td>
81: <td>
82: <%: item.Phone %>
83: </td>
84: <td>
85: <%: item.PostalCode %>
86: </td>
87: <td>
88: <%: item.Region %>
89: </td>
90: </tr>
91:
92: <% } %>
93:
94: </table>
95: <p>
96: <%: Html.ActionLink("Create New", "Create") %>
97: </p>
98: </asp:Content>
99:
設計畫面會長這個樣子,畫面如下:
但目前這還是不是我要的,因為上面並沒有Form,也沒有[客戶公司名] 與 [所在位置]這兩個查詢條件,因此稍做修改,翻一下保哥的書了解MVC的Form可使用 using (Html.BeginForm()) 的方式宣告,如下:
1: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
2:
3: <h2>ViewPage1</h2>
4: <% using (Html.BeginForm()) { %>
5: <table cellpadding="0" cellspacing="0" border="1">
6: <tr>
7: <td>客戶公司名稱</td>
8: <td><%=Html.TextBox("txtSearch") %></td>
9: </tr>
10: <tr>
11: <td>所在城市</td>
12: <td><%=Html.DropDownList("ddlCity", (IEnumerable<SelectListItem>) ViewData["ddlCity"]) %></td>
13: </tr>
14: </table>
15: <input id="Submit1" type="submit" value="submit" />
16: <% } %>
17: </asp:Content>
查詢條件的部分,當然就使用<%=Html.TextBox("txtSearch") %>與 <%=Html.DropDownList("ddlCity"> 的描述語法來完成。這裡我們還要注意一點,Submit之後才要將資料帶回來,而Submit是HTTP的POST動作,這裡MVC也提供了一個Attribute 如下:
[AcceptVerbs(HttpVerbs.Post)]
可讓我們將此接收POST的Controller部分拆開來寫,以免在同一個Controller方法中有過多複雜的判斷。還有,前面提到,為了使強行別的檢視也能夠接受既有DAL回傳的DataTable物件,因此筆者另外寫了一個QueryModelHelper<要轉換的Model>.GetModel(Model [要轉換的Model物件], DataTable [資料表])方法,筆者最後再列出此方法的內容。這時筆者撰寫完畢的CustomerController.cs程式碼如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using System.Web.Mvc;
6: using CustomerBiz;
7: using CustomerBiz.Data;
8: using CustomerBiz.Model;
9: using System.Data;
10: using MvcApplication1.Models;
11:
12: namespace MvcApplication1.Controllers
13: {
14: public class CustomerController : Controller
15: {
16: //
17: // GET: /Customer/
18: List<CompanyCityModel> GetCompanyCityModel()
19: {
20: CompanyCityModel cus = new CompanyCityModel();
21: eCustomerBiz biz = new eCustomerBiz();
22: DataTable dt = biz.GetCompanyCity();
23: List<CompanyCityModel> result = QueryModelHelper<CompanyCityModel>.GetModel(cus, dt);
24: return result;
25: }
26: DAL db = new DAL();
27: public ActionResult Index()
28: {
29: ViewData["txtSearch"] = "ALFKI";
30: ViewData["ddlCity"] = new SelectList(GetCompanyCityModel(), "CITY", "CITY");
31: return View(new List<Customers>());
32: }
33: [AcceptVerbs(HttpVerbs.Post)]
34: public ActionResult Index(string txtSearch, IEnumerable<SelectListItem> ddlCity)
35: {
36: object search = this.HttpContext.Request.Form["txtSearch"];
37: object selectedValue = this.HttpContext.Request.Form["ddlCity"];
38: ViewData["ddlCity"] = new SelectList(GetCompanyCityModel(), "CITY", "CITY", selectedValue);
39:
40: eCustomerBiz biz = new eCustomerBiz();
41: DataTable dtResult = biz.GetCustomersByID_City(search.ToString(), selectedValue.ToString());
42: List<Customers> list = QueryModelHelper<Customers>.GetModel(new Customers(), dtResult);
43: return View(list.ToList());
44: }
45: }
46: }
各位應該有發現程式中多了一個GetCompanyCityModel()方法,那是筆者為了產生[所在程式]的下拉選單另外撰寫的,同時也共用了QueryModelHelper,QueryModelHelper的類別的程式碼如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using System.Data;
6: using System.Reflection;
7:
8: namespace MvcApplication1
9: {
10: public class QueryModelHelper<T>
11: {
12: public static List<T> GetModel(T model, DataTable dtInput)
13: {
14: Type t = model.GetType();
15: PropertyInfo[] property = t.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);
16:
17: List<T> ttt = new List<T>();
18:
19: for (int i = 0; i < dtInput.Rows.Count; i++)
20: {
21: DataRow dr = dtInput.Rows[i];
22:
23: Type tc = model.GetType();
24: object o = Activator.CreateInstance(tc);
25:
26: foreach (PropertyInfo p in property)
27: {
28: foreach (DataColumn col in dtInput.Columns)
29: {
30: if (p.Name.ToUpper() == col.ColumnName.ToUpper())
31: {
32: PropertyInfo pp = o.GetType().GetProperty(p.Name);
33: if (col.DataType == typeof(System.Int64))
34: {
35: pp.SetValue(o, dr[col.ColumnName] == DBNull.Value ? null : dr[col.ColumnName], null);
36: }
37: else
38: {
39: pp.SetValue(o, dr[col.ColumnName] == DBNull.Value ? "" : dr[col.ColumnName], null);
40: }
41: }
42: }
43: }
44: ttt.Add((T)o);
45: }
46: return ttt;
47: }
48: }
49: }
這時程式已經可以執行,執行結果如下:
接著點選查詢,結果如下:
參考資料:
保哥的ASP.NET MVC 2開發實戰
ASP.Net MVC的 - 下拉列表相關聯的對象
http://zh-tw.w3support.net/index.php?db=so&id=627447
留言
張貼留言