第6章EEP 2019基礎設計(下)第6章EEP 2019基礎設計(下)\6-2 Server Method設計

6-2 Server Method設計

以前在舊時代裡,程式都是在單機上執行並完成,自從MicrosoftWindows上開立DCOMDistributed Component Object Model)的規格後,個人電腦就進入多台協同工作的時代,畢竟個人電腦的速度多快,進步神速,如果能讓各PC能協同工作,那一定是一個驚人的世界,於是近年來流行3-TierN-Tier就是這樣來的,利用各自分工的概念,讓工作站伺服器各司所長,進而讓PC能發揮出極限的效能。

.NET中,MicrosoftDCOM再次加強演化成.Net Remoting,這個技術是為了取代原有的DCOM技術,並針對DCOM的缺點加以改善,讓ClientServer間更容易被呼叫,更穩定的被執行,簡單的說,就是更容易讓不同電腦的程式可以被相互叫用,如A PC可以呼叫B PC的某個程式去執行,這樣的觀念是否與傳統完全不一樣,也改變了我們程式設計的習慣。

也因為因應Internet時代的來臨,遠端的Client都遠在Ineternet的另一端,因此如果要執行一個大量數據的處理,就不可能將資料下載到Client端處理完再送回Server端儲存,這個部份就好像是Database Server上的「預存程式」Stored Procedure類似,但因此Stored Procudure會大量佔用資料庫主機的資源,而使用.Net Remoting的處理方式比較不會佔用Database Server的資源,因此成為繁複型資料處理的主流架構。EEP2019.Net Remoting端的服務程式,命名為伺服程式(Server Method),可以提供任何Client來提供主機服務,這部份就好像是Web Service一樣,但卻有比Web Service速度快很多(因為不用格式化XML傳遞方式,直接以二進位來傳遞),以下我們用三個實例來說明Server Method的應用。

 

q   Server端新增資料

在實際應用中,很多地方都會用到因某種需要而由程式自行對資料做更改會新增資料。因為這種更改跟User沒有直接關係,所以不需要輸入畫面。下面的例子就是通過CallMethod實現自行新增資料。我們將要配合使用現有Server上的AutoNumber元件一起搭配使用,將資料新增到Purchases中。

Step1我們從SMasterDetail模版再新建一個項目S005。步驟依次設定,如下圖。


6-2-1新增S005項目

 

Step2點兩下S005Component.cs,打開Component.cs的設計畫面。依之前的教學文件一樣的步驟設定InfoConnection連接到ERPS資料庫。

6-2-2設定InfoConnection連接庫

 

Step3點兩下MasterInfoCommand),會出現設定的視窗,將Purchases的全部項目都Select

6-2-3添加Master檔語句

 

Step4點兩下DetailInfoCommand),將[Purchase Details]的全部項目都Select

6-2-4添加Detail檔語句

 

Step5設定idsRelationInfoDataSourceMasterColumnsPurchaseID再設定DetailColumns也是PurchaseID。代表Master表單以PurchaseID項目和Detail表單的PurchaseID項做關聯。


6-2-5設定idsRelationMaster/DetailColumns

 

Step6Toolbox增加一個AutoNumberS005Component.cs中。在之前的S004項目中曾介紹Purchase自動編號的首碼,規則為“P”加上6位的日期字串,但這裏示範的方式是不使用AutoNumberGetFixed屬性中設定的函數,而是直接寫在ServerMethod中,詳細見下面的步驟。AutoNumber的屬性設置如下:

6-2-6設定AutoNumber的屬性

 

Step7開發一個能新增資料的ServerMethod,所有的Server Method都必須在ServiceManager元件中要有定義。因此我們在ServiceManagerServiceCollection中增加一個ServerMethod,命名為MyInsert

6-2-7增加一個ServerMethod

 

每個ServiceCollection都有如下屬性:

F   NonLogin(是否需要登陸)

如果為True,則代表這個Method不需Login就可以被Client端直接呼叫,如果為False則必須由User Login後才能被Clieint呼叫,注意,NonLoginTrue時,會有安全上的顧慮,因為只要Client可以連到此A/P Server而且知道Method Name,就可以使用此Mehotd,所以如果非用不可,一定要考慮到安全性的問題。

F   ServiceName(服務名稱)

開放給Client用戶的Method名稱,Client通過此屬性的值來呼叫本服務,一般來說,ServiceNameDelegateName最好一樣,以免徒增困擾。

F   DelegateName(委託服務名稱)

代表實際的Method名稱,即寫在程式中的真正函數名稱。

 

Step8在此將ServiceNameDelegateName的內容都設為MyInsertNonLogin設為False。如下圖。

6-2-8定義ServiceCollection

Step9S005Component.cs中,點擊右鍵View Code,並在「Component Designer generated code」上方加入MyInsert這個程式名稱,程式如下:

 

publicobject MyInsert(object[] param)
        {
IDbConnection conn = this.AllocateConnection
("ERPS");
//
DataModule要一個Connection
if (conn.State !=ConnectionState.Open)

            {
 conn.Open();
            }

IDbTransaction trans = conn.BeginTransaction
();
//
下達Begin Transaction, 確保交易完整
object[] ret = newobject[] { 0, "Y", null }; //設定傳回值
int count = Convert.ToInt32(param[0]); //取得要Insert的筆數,由Client端傳入
try
            {
object no = null;
string nos = "
( ''";
for
(int i = 0; i < count; i++)
                {
                 no=autoNumber1.Execute
(conn,trans, string.Format("P{0:yyMMdd}", DateTime.Now.Date)); //同樣去呼叫AutoNumber,生成新的No
this.ExecuteCommand("Insert into Purchases (PurchaseID,SupplierID,EmployeeID) Values ('" + no.ToString() + "',1,1)", conn, trans);
//
記錄新增的PurchaseID,以便Client端可以查詢
nos = nos + "," + "'" + no + "'";
                }
                trans.Commit
(); // 成功後執行Commit
                nos = nos + ")";
                ret[2] = nos;
            }
catch
            {
//
如果新增失敗,則呼叫RollBack,並將錯誤訊息傳回給Client
                ret[0] = 1;
                ret[1] = "N";
                ret[2] = "Inserting Canceled!";
                trans.Rollback
();
            }
this.ReleaseConnection
("ERPS", conn); //別忘了最後要釋放Connection讓別人使用
return ret;
        }


完成之後如下圖:

6-2-9編輯MyInsert程式

 

Step10編譯S005。並在EEPNetServer->Package Manager中將其加入。


6-2-10編譯S005

 

Step11同樣以CSingle模版新建一個Client端的項目C005


6-2-11新建ClientC005項目

Step12打開Form1的設計畫面,因為只是要實作Client端的CallMethod,而不需要對資料做更改與刪除,所以我們不使用InfoNavigator並將其刪除掉。

6-2-12刪除InfoNavigator

 

Step13設定MasterInfoDataSet)的RemoteNameS005.Master,再設定ActiveTrue

6-2-13設定並開啟Master

Step14設定ibsMasterInfoBindingSource)的DataMemberMaster

6-2-14設定ibsMaster

 

Step15在此我們貼入一個TabControl控制項,先將TabControl在整個Form上的位置佈局為撐滿整個Form,因此在Dock屬性中設定為Fill。然後,通過『TabPages』屬性設定第一個TabControl頁籤上設定『Text』為【MyInsert】。

6-2-15設定TabControl屬性

 

Step16依次貼入元件。

Œ  貼入一個InfoDataGridView,命名為dgvData用於顯示新增後的資料,範圍做適當的調整,位置放在Form1的下方。

  貼入一個Label,『Text』屬性設置為【新增數量】。

Ž  貼入一個InfoTextBox,命名為tbCount,用於User輸入需要新增多少筆資料。

  最後貼入一個Button,命名為btInsertText屬性設為Insert。如圖所示:

6-2-16貼入元件

 

Step17設定dgvDataInfoDataGridView)的Data SourceibsMaster

6-2-17 設定dgvData Data Source

 

Step18btInsertClick事件(點兩下btInsert元件)中寫下如下代碼:

privatevoid btInsert_Click(object sender, EventArgs e)
        {    //
呼叫S005 MyInsert 方法,並將要新增的數量參數傳給Server
object[] back = CliUtils.CallMethod("S005", "MyInsert", newobject[] { tbCount.Text });
if
(back[1].ToString() == "Y")
            {    //
如果回傳的參數為Y,表示成功,並將所有新增的資料顯示出來
             Master.SetWhere("PurchaseID in " + back[2].ToString());
            }
else//
如果回傳失敗,則將失敗訊息ShowUser
MessageBox.Show(back[2].ToString());
        }

 

Step19編譯C005,並在EEPManager中增加此功能項,然後以EEPNetClient來執行。

6-2-18編譯C005

 

如下圖,在新增數量上輸入3,並按下「Insert」按鈕後,就會觸發A/P Server上執行S005.MyInsert程式,並讓Client可以顯示出剛才Insert的資料於Grid中。

6-2-19執行結果

 

 

q   Server端進行資料整理

當有大量資料要進行統計與分析時,如果可以用一個單一的SQL語句即可取得結果時,則就可以直接由Client端的InfoDataSet透過InfoCommand來取得資料即可,但是,如果不能以單一的SQL語句直接取得資料,尚要在這些資料上進行大量的加工與整理時,就不能將資料下載到Client再進行處理,因為下載的資料也許是很大量的資料,會造成頻寬上的負荷,由於A/P Server往往與Database Server是在同一個網域當中,因此利用Server MethodA/P Server上執行是值得肯定的。以下我們舉例是為了統計某年度,各產品每月的銷售額。統計年度由UserClient端傳入,並在Server端做好統計後,將統計的結果回傳至Client端,並顯示出來。設計步驟如下:

Step1用之前設計的S005,貼入一個InfoCommand,命名為Temp,作為統計要下SQL語句所使用。


6-2-20 貼入InfoCommand元件

 

Step2ServiceManager中再添加一個ServerMethod,命名為OrderStatistic

6-2-21貼入ServerMethod元件

Step3將程式寫在OrderStatistic函式之下,程式碼如下:

publicobject OrderStatistic(object[] param)

        {   object[] ret = newobject[] { 0, null, null };

string sql = "";  // ProductID取得每個月的訂單金額統計(UnitPrice*Quantity),每月一筆:

            sql = "Select [Order Details].ProductID, Products.ProductName, Month(OrderDate) as Mon \n" +

", Sum([Order Details].UnitPrice * [Order Details].Quantity) as OrderAmt \n" +

" From [Order Details] \n" + "Left join Orders On Orders.OrderID = [Order Details].OrderID \n" +

"Left join Products On Products.ProductID = [Order Details].ProductID \n" +

"Where Year(OrderDate) = " + param[0].ToString() + " \n" +

"Group by [Order Details].ProductID, Products.ProductName, Month(OrderDate) \n" +

"Order by [Order Details].ProductID, Mon";

DataTable dtOrderData = ExecuteSql("Temp", sql, "ERPS", true).Tables[0];

if(dtOrderData.Rows.Count == 0)

            {  //如果為空就返回

                ret[0] = 0;

                ret[1] = null;

                ret[2] = "There is no data!";

return ret;

            }   // 取得一個空的自定結構資料表,包含到月的統計欄位

            sql = "Select ProductID, ProductName, 0.0 as TotalAmt \n" +

", 0.0 as M01, 0.0 as M02, 0.0 as M03, 0.0 as M04, 0.0 as M05, 0.0 as M06 \n" +

", 0.0 as M07, 0.0 as M08, 0.0 as M09, 0.0 as M10, 0.0 as M11, 0.0 as M12 \n" +

"From Products \n" + "Where 1=0 \n" + "Order by ProductID";

DataTable dtOrder = ExecuteSql("Temp", sql, "ERPS", true).Tables[0];

DataRow drData = null;

string Mon = "";

for(int j = 0; j < dtOrderData.Rows.Count; j++)

            {  DataRow[] drs = dtOrder.Select("ProductID = \'" + dtOrderData.Rows[j][0].ToString() + "\'");

// 用目前這筆dtOrderDataPruductID去找dtOrderProductID

if(drs.Length == 0)

                {   // 找不到則對dtOrder新增一筆資料

                    drData = dtOrder.NewRow();

                    drData["ProductID"] = dtOrderData.Rows[j]["ProductID"].ToString();

                    drData["ProductName"] = dtOrderData.Rows[j]["ProductName"].ToString();

                    drData["TotalAmt"] = 0;

for(int i = 1; i < 13; i++)

                    {   Mon = i.ToString();

if(Mon.Length < 2)

                            Mon = "M0" + Mon;

else

                            Mon = "M" + Mon;

                        drData[Mon] = 0;

                    }

                    dtOrder.Rows.Add(drData); // drData InsertdtOrder

                }

else

                {  drData = drs[0]; // 取出找到的第一筆

                }

                Mon = dtOrderData.Rows[j]["Mon"].ToString();  //取出月份

if(Mon.Length < 2)

                    Mon = "M0" + Mon;

else

                    Mon = "M" + Mon;

       drData[Mon] = Convert.ToDecimal(drData[Mon]) + Convert.ToDecimal(dtOrderData.Rows[j][3].ToString());
//
累計到相對的月份欄位

       drData["TotalAmt"] = Convert.ToDecimal(drData["TotalAmt"]) + Convert.ToDecimal(dtOrderData.Rows[j][3]); //累計到TotalAmt欄位

            }

            ret[1] = dtOrder;

            ret[2] = "";

return ret;

        }

 

Step4編譯S005。在C005Form1,將原來TabControl的第二個TabPage的『Text』設為【OrderStatistic】。

6-2-22/1 編譯S005項目

6-2-22/2 設定TabPage2屬性

 

Step5貼入元件。

Œ  在此Tab頁中增加一個InfoDataGridView,命名為dgvOrderStatistic,用於顯示統計結果。

   再增加一個Label元件,Label的『Text』設為【統計年度:】。

Ž  再增加一個InfoTextBox元件,命名為tbYear,用於讓User輸入統計的年份。

  最後增加一個Button元件,命名為btOrderStatistic,然後將Button的『Text』設為【Monthly Report】。

6-2-23如圖貼入元件

 

Step6點兩下btOrderStatistic這個Button,在Click事件中加入如下程式:

privatevoid btOrderStatistic_Click(object sender, EventArgs    

         {

object[] back = CliUtils.CallMethod("S005", "OrderStatistic", newobject[] { tbYear.Text }); //將年度傳到Server

//如果統計有資料,在將資料放在dgOrderStatistic中顯示,否則顯示錯誤資訊。:

if(Convert.ToInt16(back[0]) == 0)

            {

                       if(back[1] != null)

{

      dgvOrderStatistic.DataSource = back[1];

}

else

{

   MessageBox.Show(此年份的資料為空!);

}

            }

else

MessageBox.Show(back[2].ToString());

     }

 

Step7編譯C005,並執行EEPNetClient。在「統計年度」中輸入【1997】年,按下「Monthy Report」這時會執行OrderStatistic這個Server Method,並傳回所要的統計結果於DataGridView中,如下圖。

6-2-24執行

 

 

q   非同步Server Method設計

一般Client端呼叫Server Method都是用同步的方式呼叫,所謂同步,就是Client端呼叫後會去等待Server端的Method處理完畢並傳回值後才會讓Client繼續工作,大部份的Server Method不會執行過長的時間(如數秒內),因此Client等待是合理而且是可以接受的,但如果因為特殊的情況下,Server Method會執行很久(如數分鐘以上),此時如果讓Client端一直等待下去其實是不符合人性的,因此在EEP2019中我們另外提供了非同步的Server Method機制,讓Client呼叫後,可以不必等待執行結果,並可以繼續進行別的工作,等ServerMethod執行完畢後,會另外通知Client端,來改善與突破傳統等待服務的缺失,我們同樣舉一個實例來說明,就是統計某段時間內,Order金額最多的10個客戶,並記錄這10個用戶分別購買最多的10個產品的總金額,顯示時我們要將客戶以橫向顯示(X軸),產品則以直向顯示(Y軸),如此可以看到客戶Order產品的交叉統計資料。

 

Step1同樣用S005,在ServiceManager中增加一個ServerMethod,命名為CustStatistic。程式如下,請加在Component Designer generated code的上方

publicobject CustStatistic(object[] param)        

{         object[] ret = newobject[] { 0, null, null };

//dtCust : 找出個在這段時間範圍中購買金額最多的客戶,目前是直向關係

string sql = "";

DateTime d1 = Convert.ToDateTime(param[0]);

DateTime d2 = Convert.ToDateTime(param[1]);

String d1s = d1.ToShortDateString();

String d2s = d2.ToShortDateString();

            sql = "Select Top 10 Orders.CustomerID, \n"+"Sum(UnitPrice*Quantity*(1-Discount)) as Amt \n" +"From Orders \n" +"Left join [Order Details] on [Order Details].OrderID = Orders.OrderID \n" +"Where OrderDate Between '" + d1s + "' And '" + d2s + "' \n" +"Group by Orders.CustomerID \n" +"Order by Amt Desc";

DataTable dtCust = ExecuteSql("Temp", sql, "ERPS", true).Tables[0];

if(dtCust.Rows.Count == 0)

            {  // 如果沒有資料就返回並顯示錯誤訊息.

                ret[1] = "There is no data!";

return ret;

            }

// 用一個SQL語句來產生一個空的資料表結構, 來分別存放不同客戶所購買產品,其中N01N10是代表第一個客戶到第十個客戶(橫向),最後會將此數據傳回client

            sql = "Select ProductID, ProductName,'0.00' as NO1, '0.00' as NO2 \n" +", '0.00' as NO3, '0.00' as NO4, '0.00' as NO5, '0.00' as NO6 \n" +", '0.00' as NO7, '0.00' as NO8, '0.00' as NO9, '0.00' as N10 \n" +"From Products Where 1=0 ";

DataTable dtData = ExecuteSql("Temp", sql, "ERPS", true).Tables[0];

DataRow drData = dtData.NewRow();

//dtTemp :用來抓每個客戶購買的前十大產品

DataTable dtTemp = newDataTable();

// 依前十大客戶逐一處理

for(int i = 0; i < dtCust.Rows.Count; i++)

            {

                dtData.Columns[i + 2].ColumnName = dtCust.Rows[i][0].ToString();

// 將準備返回的dtData欄位名稱改成以客戶編號做為欄位名稱(這樣Client端的DataGridViewHeader也會顯示此客戶編號), 從第個欄位開始改, ,2個欄位原本為ProductIDProductName

                sql = "Select Top 10 [Order Details].ProductID, ProductName, \n" +"Sum([Order Details].UnitPrice*Quantity*(1-Discount)) as Amt \n" +"From Orders, [Order Details] \n" +"Left join Products on Products.ProductID = [Order Details].ProductID \n" +"WhereOrders.OrderID= [Order Details].OrderID \n" +" and Orders.OrderDate Between '" + d1s + "' and '" + d2s + "'\n" +" and Orders.CustomerID = '" + dtCust.Rows[i][0].ToString() + "'\n" +"Group by [Order Details].ProductID, ProductName \n" +"Order by Amt Desc";

                dtTemp = ExecuteSql("Temp", sql, "ERPS", true).Tables[0];

// dtTemp中存在此客戶的前十訂購產品金額,依次將這些產品放入相對的客戶位置中

for(int j = 0; j < dtTemp.Rows.Count; j++)

                {

//dsData(一開始為空)中查詢是否已經存在該產品

DataRow[] drs = dtData.Select("ProductID = '" + dtTemp.Rows[j][0].ToString() + "'");

if(drs.Length == 0)

                    {   // 不存在,則新增一筆dsData,並存放ProductID,ProductName

                        drData = dtData.NewRow();

                        drData[0] = dtTemp.Rows[j][0].ToString(); //ProductID

                        drData[1] = dtTemp.Rows[j][1].ToString(); //ProductName

                        drData[i + 2] = dtTemp.Rows[j][2].ToString(); //按客戶次序放入產品統計值

                        dtData.Rows.Add(drData);

                    }

else

                    {  // 存在,依客戶次序放入產品統計值(橫放)

                        drData = drs[0];

                        drData[i + 2] = dtTemp.Rows[j][2].ToString();

                    }

                }

            }

            ret[1] = "";

            ret[2] = dtData;

return ret;

}

 

Step2編譯S005

6-2-25編譯S005項目

Step3打開C005專案的Form1的設計畫面,在原來TabControl中添加一個TabPageText設為CustStatistic


6-2-26設定TabPage3屬性

 

Step4貼入元件。

Œ   在此Tab頁中增加一個RichTextBox,用於顯示統計結果。

  再貼入兩個InfoDateTimePicker,分別命名為dateFromdateTo

Ž  以及兩個Label,第一個Label的『Text』設為【統計開始日期】,第二個Label的『Text』設為【統計結束日期】。

 再貼入一個ButtonName設為btRunMethodText設為Run Method

6-2-27貼入元件

 

Step5ButtonClick事件中加入如下程式:

privatevoid btRunMethod_Click(object sender, EventArgs e)

        {

            richTextBox1.Text = "";

CliUtils.AsyncCallMethod("S005", "CustStatistic", newobject[] { dateFrom.Value, dateTo.Value }, MyCallBack);

//這是非同步的呼叫方式,因此還要定義MyCallBack
        }

 

非同步呼叫與同步呼叫是不同,同步呼叫需等待Server端執行完成,而非同步呼叫無需等待,在Server端執行Method完成後,才會通知Client,而通知的方式就是上例中的MyCallBack,一個自定義的函數,就是指Server端執行完畢後所要通知Client的程式進入點。所以下列是MyCallBack的程式,如下:(可以加在 privatevoid btRunMethod_Click(object sender, EventArgs e)上方)

publicvoid MyCallBack(object[] oRet)

        {

if(Convert.ToInt16(oRet[0]) == 0&&oRet[2] != null)

            {

 

                System.Data.DataTable retData =(System.Data.DataTable)oRet[2];

foreach(DataRow row in retData.Rows)

                {

foreach(DataColumn column in retData.Columns)

                    {

                        richTextBox1.Text = richTextBox1.Text + column.ColumnName.ToString();

                        richTextBox1.Text = richTextBox1.Text + " : ";

                        richTextBox1.Text = richTextBox1.Text + row[column].ToString();

                        richTextBox1.Text = richTextBox1.Text + " | ";

 

                    }

                    richTextBox1.Text = richTextBox1.Text + "\n\n";

                }

 

            }

else

MessageBox.Show(oRet[1].ToString());

 

        }



Step6編譯C005項目,執行EEPNetClient.EXE。打開C005,在日期中輸入【1996/1/1】到【2000/12/31】,再按「Run Method」(Button),此時執行Server Method時不會去等待,一直到最後結果返回到MyCallBack()將結果顯示在richTextBox1.Text上。

6-2-28執行結果

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Top of Page