二十八、帶給我們一種新的編碼思路——EFW框架CS系統開發中的MVC模式探討


 回《【開源】EFW框架系列文章索引》       

 EFW框架源代碼下載V1.3:http://pan.baidu.com/s/1c0dADO0

 EFW框架實例源代碼下載:http://pan.baidu.com/s/1eQCc69G

 

      前言:記得最初寫出Winform版MVC的代碼是在公司的一個產品中,產品有幾個界面功能比較多,一個界面窗體的代碼盡然有1萬多行代碼,讓我們在維護這幾個界面的時候非常的痛苦,你可能想可以把這個大的界面拆分成幾個小的界面在集成在一起不就好了,但實際上這樣行不同,首先界面上的控件之間依賴性太強不好拆分,更主要的是大量代碼是針對網格控制的操作;后來我和另一個同事覺得重構這幾個界面,同事也是一個對技術比較痴迷的那種,他利用委托來實現邏輯代碼與界面之間的分離,針對界面中的控件操作定義一系列委托,再另外建一個對象編寫業務邏輯並將數據通過委托在界面上顯示;這種方式也達到了分離界面代碼的目的,但寫代碼總感覺比較別扭,委托太多了根本搞不清楚,代碼寫起來也復雜,要弄清楚之間的調用關系不容易;而我參考了一下網上MVC的設計模式,建了一個控制器的對象用來封裝所有業務邏輯代碼,再把界面的所有數據操作封裝成一個接口,控制器通過調用接口的方式對界面取數據和返回數據;對比起上面的委托方式,確實代碼更簡單,而且思路清晰,起碼接口比委托封裝性要好,所有的數據操作都可以封裝在一個接口里;這樣以來Winform控制器這種模式就初步成形了;通過使用此設計,讓原來1萬多行的界面代碼縮減到只有幾千行,就算加上控制器的代碼也比原來少了一半不止;這就是Winform控制器的神奇之處,當初寫完連自己都不相信;

       后來在項目實踐這種開發模式的過程中,不斷的完善總結,也形成了一套內部約定吧,比如對界面接口該如何定義,復雜的業務邏輯中控制器對象又怎么划分等等,這些不太容易成文的東西達成了一種共識或理解;覺得一種設計方式不是說一下就能寫出來的,也不是說從書本上看到某個設計就能拿過來用的;這都只是帶給你靈感,促進你思考,而真要領悟它必須得在長期的實踐中積累,一定得多寫代碼,反復的重構,這樣它才會成為屬於自己的開發模式,才能更好的傳播給他人;

本文要點:

1.Winform版MVC介紹

2.Winform版MVC使用實例

3.針對“程序=結構+算法”中的“結構”分析

4.控制器與界面之間的關系以及一些設計原則

5.帶給我們一種新的編碼思路

 

1.Winform版MVC介紹

Winform版MVC跟Web版類似,目的都是分離界面和后台邏輯代碼,是一種開發模式,

Model:就是ObjectModel、Dao和Entity

View:就是WinForm

Controller:就是WinController

 

       但是與Web版也有不同的地方,Winform版的界面與控制器關系更緊密、也更加靈活,比如界面上數據聯動,Web版的話必須利用Ajax發送多次請求,而Winform版不管有多少次數據聯動界面上不用處理,控制器可以自由控制界面上數據展示;這也是Winform版MVC與Web版MVC根本上的區別;另外,Winform版多了一個界面接口封裝了界面數據,而界面接口的設計好壞充分體現了對MVC模式的理解深度;本章主要內容也是講解界面層與控制器直接的關系。

 

2.Winform版MVC使用實例

實例還是用書籍管理來說明,一個界面維護書籍目錄,實現書籍的添加、修改、刪除和查詢;

界面效果

 

frmBookManager界面文件

 1 public partial class frmBookManager : BaseForm, IfrmBook
 2     {
 3         public frmBookManager()
 4         {
 5             InitializeComponent();
 6 
 7             frmForm.AddItem(txtbookname, "BookName","必須輸入書籍名稱!");
 8             frmForm.AddItem(txtprice, "BuyPrice");
 9             frmForm.AddItem(txtdate, "BuyDate");
10             frmForm.AddItem(ckflag, "Flag");
11 
12             txtdate.Value = DateTime.Now;
13         }
14 
15 
16         #region IfrmBook 成員
17 
18         public void loadbooks(DataTable dt)
19         {
20             gridBook.DataSource = dt;
21         }
22 
23         private Book _book;
24         public Books.Entity.Book currBook
25         {
26             get
27             {
28                 frmForm.GetValue<Book>(_book);
29                 return _book;
30             }
31             set
32             {
33                 _book = value;
34                 frmForm.Load<Book>(_book);
35             }
36         }
37 
38         public void DrawPie(DataTable dt, string title)
39         {
40             DataTable tbData = dt;
41             TableColumn[] columns = new TableColumn[1];
42             columns[0].ColumnName = "時間";
43             columns[0].ColumnField = "num";
44             GraphControl gc;
45             DataTableStruct datatablestruct = DataTableStruct.Rows;
46             Color[] colors = new Color[tbData.Rows.Count];
47             Random random = new Random();
48             for (int index = 0; index < tbData.Rows.Count; index++)
49             {
50                 int red = random.Next(255);
51                 int blue = random.Next(255);
52                 int green = random.Next(255);
53                 colors[index] = Color.FromArgb(red, green, blue);
54             }
55             //餅圖
56             gc = new CakyGraphControl(this.panelPie, datatablestruct, columns, colors, tbData, "BuyDate", 0);
57             gc.GraphTitle = title;
58             gc.DrawGraph();
59         }
60 
61         #endregion
62         //選擇書籍
63         private void gridBook_Click(object sender, EventArgs e)
64         {
65             if (gridBook.CurrentCell != null)
66             {
67                 int rowindex = gridBook.CurrentCell.RowIndex;
68                 DataTable dt = (DataTable)gridBook.DataSource;
69                 //
70                 int Id = Convert.ToInt32(dt.Rows[rowindex]["Id"]);
71                 _book = new Book();
72                 _book.Id = Id;
73                 //取出網格數據賦值給控件
74                 frmForm.Load(dt.Rows[rowindex]);
75             }
76         }
77         //新增
78         private void btnadd_Click(object sender, EventArgs e)
79         {
80             //清空右邊面板控件數據
81             _book = new Book();
82             
83         }
84         //保存
85         private void btnsave_Click(object sender, EventArgs e)
86         {
87             if (frmForm.Validate())
88             {
89                 InvokeController("bookSave");
90             }
91         }
92         //導出Excel
93         private void btnExport_Click(object sender, EventArgs e)
94         {
95             InvokeController("ExportExcel");
96         }
97 
98         
99     }
View Code

 

IfrmBook界面接口文件

1  public interface IfrmBook : IBaseView
2     {
3         //給網格加載數據
4         void loadbooks(DataTable dt);
5         //當前維護的書籍
6         Book currBook { get; set; }
7         //畫餅圖
8         void DrawPie(DataTable dt, string title);
9     }
View Code

 

bookwinController控制器文件

 1 [EFWCoreLib.WinformFrame.Controller.Menu(DefaultName = "bookmenu", DefaultViewName = "frmBookManager")]//與系統菜單對應
 2     [View(Name = "frmBookManager", DllName = "Books.Winform.dll", ViewTypeName = "Books.Winform.Viewform.frmBookManager")]
 3     public class bookwinController : BaseController
 4     {
 5         IfrmBook frmBook;
 6         public override void Init()
 7         {
 8             frmBook = (IfrmBook)DefaultView;
 9             //初始化加載書籍目錄
10             GetBooks();
11             GetPie();
12         }
13 
14         //獲取書籍目錄
15         public void GetBooks()
16         {
17             IBookDao bdao = NewDao<IBookDao>();
18             DataTable dt = bdao.GetBooks("", 0);
19             frmBook.loadbooks(dt);
20         }
21         //保存
22         public void bookSave()
23         {
24             frmBook.currBook.BindDb(oleDb, _container);
25             //從界面獲取數據保存
26             frmBook.currBook.save();
27             //從數據庫獲取數據顯示在界面上
28             GetBooks();
29         }
30 
31         //導出Excel
32         public void ExportExcel()
33         {
34             IBookDao bdao = NewDao<IBookDao>();
35             DataTable dt = bdao.GetBooks("", 0);
36             Dictionary<string,string> dicCol=new Dictionary<string,string>();
37             dicCol.Add("BookName", "書籍名稱");
38             dicCol.Add("BuyPrice", "價格");
39             dicCol.Add("BuyDate", "購買時間");
40             ExcelHelper.Export(dt,"書籍目錄",dicCol,"c:\\books.xls");
41         }
42 
43         //查詢數據畫餅圖
44         public void GetPie()
45         {
46             string strsql=@"SELECT CONVERT(varchar(100), BuyDate, 23) BuyDate,COUNT(*) num FROM dbo.Books GROUP BY CONVERT(varchar(100), BuyDate, 23) ";
47             DataTable dt=oleDb.GetDataTable(strsql);
48             frmBook.DrawPie(dt, "按時間書籍數量");
49         }
50     }
View Code

 

3.針對“程序=結構+算法”中的“結構”分析

      “程序=結構+算法”,其中“算法”同等於邏輯代碼,而“結構”分為三個方面,數據庫表結構、業務對象與實體、界面控件綁定數據源結構。而這三方面在程序中相互轉換,利用框架中ORM可以把數據庫表數據轉換為實體集合,把實體集合通過數據源綁定在DataGridView控件上顯示;界面控件通過賦值轉換為實體對象,實體對象通過數據庫操作對象保存到數據庫表中;所以代碼對於“結構”的封裝與轉換非常頻繁,結構處理得越好,那么系統也就越清晰。實體與數據庫直接的轉換我們可以通過框架中的ORM來解決,而界面控件與業務實體直接轉換一般都很隨意,以至於賦值與取值代碼到處都是,經常跟邏輯層代碼混在一起,使我們后面對代碼的理解與維護都帶來了很多麻煩,所以需要一種好的開發架構來解決這個問題,而MVC模式就是不錯的選擇,使用界面接口把界面控件與業務對象直接的轉換都封裝起來,控制器都用接口的方式來操作界面;

       以實例進行說明,先看書籍的“保存”操作,傳統的方式肯定是這樣的,在保存事件中先實例化Book對象,再把界面上的控件的值賦值給Book對象,再把Book對象通過參數傳到后台進行保存到數據中。再看界面上控件顯示書籍內容,傳統方式也是后台取出Book對象到界面,界面再一個個屬性賦值在控件上。我們再看看使用MVC模式如何實現,先在界面接口IfrmBook中定義一個currBook的屬性,界面frmBookManager繼承IfrmBook接口實現currBook屬性,在get中實現界面控制賦值給Book對象的代碼,在set中實現Book對象賦值給界面控件的代碼;這樣我們就把取值與賦值都封裝在一個屬性中,是不是很清晰,而且重用度很高;實現”保存“操作,界面只需向控制器發送一個消息,控制器自己通過接口獲取實體,再保存到數據庫;

      另外,MVC模式不止解決了“結構”上的問題,對比傳統的開發方式帶給了我們一種新的開發方式,讓我們實現功能的思路更清晰,代碼更精簡;

4.控制器與界面之間的關系以及一些設計原則

 

      Winform版的MVC與Web版的控制器與界面關系雖然都是一對多的關系,一個控制器對應多個界面,Web版中雖然支持一個界面可以分別調用多個控制器,但這種方式不太建議,這會帶來程序上的復雜度,看起來比較亂;雖然兩者關系很相似,但卻有本質上的區別,Web版一個操作要獲取兩個數據,必須利用Ajax發送兩次請求分別獲取,等於數據與數據之間的邏輯是獨立的,完全沒有交互;而Winform版的就不一樣,兩個數據界面可以單獨向控制器請求,也可以一個請求控制器返回多個數據在界面上。控制器利用界面接口可以隨意的操作界面上的數據。

既然控制器操作界面這么靈活,那么為了編碼過程中不易失控,總結了一些界面與控制器的設計原則:

1.一個控制器對應多個界面接口,一個界面接口對應一個界面

2.先執行控制器代碼再執行界面代碼,由控制器操作界面而不是界面操作控制器

3.操作界面響應事件后,不在事件代碼中實現此功能,只是發送一個消息到控制器,由控制器中調用業務邏輯實現此功能再通過界面接口返回到界面

4.界面代碼除了事件代碼與實現接口代碼,盡量不要有其他代碼

5.同一控制器中的界面之間的數據傳遞不能通過構造函數或全部變量,只能通過控制器傳遞

6.界面接口一般封裝的都是界面數據,界面數據又分為顯示數據和取值數據

7.控制器獲取界面值,除了通過接口方式,簡單的取值可以使用界面發送消息給控制器時一起發送過來

8.控制器可以通過接口調用界面,但界面不能直接調用控制器,界面只能發送消息給控制器

9.全局變量一般都定義在控制器中

10.一個界面操作同控制器的其他界面是很容易的,同一控制器下的所有界面數據都是透明的

11.如果一個界面上的控件顯示有幾個特定狀態,比如:開始和結束兩個狀態下按鈕顯示,這時可以把這個狀態封裝在界面接口中

12.像錄入數據界面有多個控件,那么對這些控件的取值和賦值不需要全部封裝成接口,可以使用實體或其他結構封裝成一個接口屬性就行了

13.界面與控制器代碼分為兩個項目的話,接口文件放在控制器項目中,界面項目引用控制器項目

 

5.帶給我們一種新的編碼思路

      在講新的編碼思路之前,先看一下傳統的編碼方法,以前一般都是先把界面畫好,再把界面上的功能一個個實現,從前台到后台,就比如“保存”功能,先在保存事件中編寫代碼,把界面控件上的值賦值給Book對象,再編寫后台一個方法,界面調用后台方法把Book對象通過參數傳遞到后台,后台方法中編寫SQL語句把Book對象保存到數據庫,再提示保存成功。實現完保存功能,可能接下來就實現查詢功能,刪除功能等。從中得出傳統實現方式就像“點”到“面”,“點”就是界面上的功能,“面”就是一個個界面;這樣做起來是很順手,但是做完后我們再看代碼就能發現一些問題,因為界面上的功能並不是完全獨立,之間肯定存在或多或少的關聯,如果剛開始不從“面”上考慮,點與點之間的代碼必然會出現重復,這樣由少集多整個代碼就會變得復雜,這樣必定為以后得維護帶來很多麻煩。也許你可以事后對這些代碼進行重構來解決這些問題,但有沒有一種好的方法事前就解決掉這個問題了?這就是我說的新的編碼思路。

      新的編碼思路簡單的說就是從“面”到“點”來編寫代碼,“面”不只是指界面,也是指控制器,“點”就是實現功能。先看一下這種方式的實現過程:

MVC模式代碼編寫過程:

1.設計好界面

2.新建控制器對象及界面接口,以及控制器與界面的關聯

3.根據界面控件抽象出界面接口方法(綁定數據到界面控件)

4.根據業務操作抽象出控制器方法(界面操作事件)

5.界面繼承接口並實現接口與界面操作事件發送消息給控制器代碼

6.到此整個代碼架子已經完成,接下來只要對控制器中的業務方法填空就行了

通過上面方式“面”中兩點把握好好后,基本后面“點”的實現只要就簡單了,兩點分別是,封裝界面接口全面考慮數據結構轉換,提取控制器方法全面考慮業務功能;

6.總結

      一般剛學習這種MVC模式的時候總是對界面接口這個文件很不理解,因為以前的方式都是界面直接調用后台方法,搞個界面接口夾在中間非常多余,這是因為剛開始對這種新的編碼思路還沒有理解,只有理解了這種新的方式與以前的區別,再在開發中考慮上面所說的設計原則,那么就能體驗到MVC模式帶來的好處。


免責聲明!

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



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