[心得分享] -(實做篇)-將現有網站改成MVC架構

先前參加了佛心Jed哥的贈書活動,小弟也獲得了保哥的新書 [ASP.NET MVC2 開發實戰]一書,書中小弟獲益良多,最近也試著將現有的專案改以MVC的架構,在實做這個改變的過程,小弟便將其記錄下來並分享給各位。一般來說,ASP.NET的網頁應用程式不外乎會有商業邏輯層(Business Tier)、資料層 (Data Tier)、網頁 (ASPX)

先前參加了佛心Jed哥的贈書活動,小弟也獲得了保哥的新書 [ASP.NET MVC2 開發實戰]一書,書中小弟獲益良多,最近也試著將現有的專案改以MVC的架構,在實做這個改變的過程,小弟便將其記錄下來並分享給各位。

一般來說,ASP.NET的網頁應用程式不外乎會有商業邏輯層(Business Tier)、資料層 (Data Tier)、網頁 (ASPX),而在網站的架構中我們可能習慣使用不同的NameSpace (資料夾)來區分商業邏輯層與資料層,基於商業機密公司應用程式不方便貼上來,小弟以Northwind資料庫實做一個範例來展現這個典型的網站架構。

這是一個客戶資料查詢的網頁程式,它是個簡單List [清單]類型的查詢的網頁程式,它有兩個查詢條件,1.客戶公司名稱、2.所在城市。查詢設計畫面如下:

image

查詢會呼叫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複製到此資料夾中,如下:

image

 

對了,一個地方忘了說明,其中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就在這個方法中間點選滑鼠右鍵,點選 [加入檢視],注意!一定要在這個方法的中間點選滑鼠右鍵才會出現[加入檢視]的選項,否則是看不見的,因為在這裡IDE工具是有特殊的支援的,不信的話您可以在其他地方選右鍵,一定看不倒此選項。

在加入View之前請在Views的資料夾下面先建立一個Customer資料夾,因為我們希望Customer的部分就放置於此,基本上控制就是模組的名稱加上Controller結尾,這個是規定,所以View的名稱也是CustomerController去掉Controller",因此為”Customer”。

那麼點選[加入檢視]後會出現如下對話框,請選擇建立強行別的檢視,並選擇我們剛才建立的Customer 的Model,並要使用List的方式,才像我們在Web Form設計畫面要的結果,如下:

image

這個動作會對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:   

設計畫面會長這個樣子,畫面如下:

image

但目前這還是不是我要的,因為上面並沒有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:  }

 

這時程式已經可以執行,執行結果如下:

image

接著點選查詢,結果如下:

image

參考資料:

保哥的ASP.NET MVC 2開發實戰

ASP.Net MVC的 - 下拉列表相關聯的對象

http://zh-tw.w3support.net/index.php?db=so&id=627447


 

簽名:

學習是一趟奇妙的旅程

這當中,有辛苦、有心酸、也有成果。有時也會有瓶頸。要能夠繼續勇往直前就必須保有一顆最熱誠的心。

軟體開發之路(FB 社團)https://www.facebook.com/groups/361804473860062/

Gelis 程式設計訓練營(粉絲團)https://www.facebook.com/gelis.dev.learning/


 

如果文章對您有用,幫我點一下讚,或是點一下『我要推薦,這會讓我更有動力的為各位讀者撰寫下一篇文章。

非常謝謝各位的支持與愛護,小弟在此位各位說聲謝謝!!! ^_^