實做篇-將現有網站改成MVC架構

先前參加了佛心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

留言

這個網誌中的熱門文章

軟體架構設計:API 設計準則(二)、API Design-First 原則、策略與開發流程

常見的程式碼壞味道(Code Smell or Bad Smell)

什麼是 gRPC ?