以前在舊時代裡,程式都是在單機上執行並完成,自從Microsoft在Windows上開立DCOM(Distributed
Component Object Model)的規格後,個人電腦就進入多台協同工作的時代,畢竟個人電腦的速度多快,進步神速,如果能讓各PC能協同工作,那一定是一個驚人的世界,於是近年來流行3-Tier與N-Tier就是這樣來的,利用各自分工的概念,讓工作站伺服器各司所長,進而讓PC能發揮出極限的效能。
在.NET中,Microsoft將DCOM再次加強演化成.Net
Remoting,這個技術是為了取代原有的DCOM技術,並針對DCOM的缺點加以改善,讓Client與Server間更容易被呼叫,更穩定的被執行,簡單的說,就是更容易讓不同電腦的程式可以被相互叫用,如A
PC可以呼叫B
PC的某個程式去執行,這樣的觀念是否與傳統完全不一樣,也改變了我們程式設計的習慣。
也因為因應Internet時代的來臨,遠端的Client都遠在Ineternet的另一端,因此如果要執行一個大量數據的處理,就不可能將資料下載到Client端處理完再送回Server端儲存,這個部份就好像是Database Server上的「預存程式」Stored Procedure類似,但因此Stored Procudure會大量佔用資料庫主機的資源,而使用.Net Remoting的處理方式比較不會佔用Database Server的資源,因此成為繁複型資料處理的主流架構。EEP2010把.Net Remoting端的服務程式,命名為伺服程式(Server Method),可以提供任何Client來提供主機服務,這部份就好像是Web Service一樣,但卻有比Web Service速度快很多(因為不用格式化XML傳遞方式,直接以二進位來傳遞),以下我們用三個實例來說明Server Method的應用。
q 在Server端新增資料
在實際應用中,很多地方都會用到因某種需要而由程式自行對資料做更改會新增資料。因為這種更改跟User沒有直接關係,所以不需要輸入畫面。下面的例子就是通過CallMethod實現自行新增資料。我們將要配合使用現有Server上的AutoNumber元件一起搭配使用,將資料新增到Purchases中。
Step1>我們從SMasterDetail模版再新建一個項目S005。步驟依次設定,如下圖。
圖 5-2-1 新增S005項目
Step2>點兩下S005的Component.cs,打開Component.cs的設計畫面。依之前的教學文件一樣的步驟設定InfoConnection連接到ERPS資料庫。
圖 5-2-2設定InfoConnection連接庫
Step3>點兩下Master(InfoCommand),會出現設定的視窗,將Purchases的全部項目都Select。
圖 5-2-3 添加Master檔語句
Step4>點兩下Detail(InfoCommand),將[Purchase Details]的全部項目都Select。
圖 5-2-4 添加Detail檔語句
Step5>設定idsRelation(InfoDataSource)的『MasterColumns』為【PurchaseID】,再設定『DetailColumns』也是【PurchaseID】。代表Master表單以PurchaseID項目和Detail表單的PurchaseID項做關聯。
圖 5-2-5設定idsRelation的Master/DetailColumns
Step6>從Toolbox增加一個AutoNumber到S005的Component.cs中。在之前的S004項目中曾介紹Purchase自動編號的首碼,規則為“P”加上6位的日期字串,但這裏示範的方式是不使用AutoNumber的GetFixed屬性中設定的函數,而是直接寫在ServerMethod中,詳細見下面的步驟。AutoNumber的屬性設置如下:
圖 5-2-6 設定AutoNumber的屬性
Step7>開發一個能新增資料的ServerMethod,所有的Server Method都必須在ServiceManager元件中要有定義。因此我們在ServiceManager的ServiceCollection中增加一個ServerMethod,命名為MyInsert。
圖 5-2-7 增加一個ServerMethod
每個ServiceCollection都有如下屬性:
F NonLogin(是否需要登陸)
如果為True,則代表這個Method不需Login就可以被Client端直接呼叫,如果為False則必須由User Login後才能被Clieint呼叫,注意,NonLogin為True時,會有安全上的顧慮,因為只要Client可以連到此A/P Server而且知道Method Name,就可以使用此Mehotd,所以如果非用不可,一定要考慮到安全性的問題。
F ServiceName(服務名稱)
開放給Client用戶的Method名稱,Client通過此屬性的值來呼叫本服務,一般來說,ServiceName與DelegateName最好一樣,以免徒增困擾。
F DelegateName(委託服務名稱)
代表實際的Method名稱,即寫在程式中的真正函數名稱。
Step8>在此將ServiceName和DelegateName的內容都設為【MyInsert】,『NonLogin』設為【False】。如下圖。
圖 5-2-8 定義ServiceCollection
Step9>在S005的Component.cs中,點擊右鍵【View Code】,並在「Component Designer generated code」上方加入【MyInsert】這個程式名稱,程式如下:
public
object 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 = new object[] {
0, "Y", null }; //設定傳回值
int count = Convert.ToInt32(param[0]); //取得要Insert的筆數,由Client端傳入
try
{
object no = null;
string nos = "(
''";
for (inti = 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;
}
完成之後如下圖:
圖 5-2-9 編輯MyInsert程式
Step10>編譯S005。並在『EEPNetServer』->『Package Manager』中將其加入。
圖 5-2-10編譯S005
Step11>同樣以CSingle模版新建一個Client端的項目C005。
圖 5-2-11新建Client端C005項目
Step12>打開Form1的設計畫面,因為只是要實作Client端的CallMethod,而不需要對資料做更改與刪除,所以我們不使用InfoNavigator,並將其刪除掉。
圖 5-2-12 刪除InfoNavigator
Step13>設定Master(InfoDataSet)的『RemoteName』為【S005.Master】,再設定『Active』為【True】。
圖 5-2-13 設定並開啟Master
Step14>設定ibsMaster(InfoBindingSource)的『DataMember』為【Master】。
圖 5-2-14 設定ibsMaster
Step15>在此我們貼入一個TabControl控制項,先將TabControl在整個Form上的位置佈局為撐滿整個Form,因此在Dock屬性中設定為Fill。然後,通過『TabPages』屬性設定第一個TabControl頁籤上設定『Text』為【MyInsert】。
圖 5-2-15 設定TabControl屬性
Step16>依次貼入元件。
Œ 貼入一個InfoDataGridView,命名為dgvData用於顯示新增後的資料,範圍做適當的調整,位置放在Form1的下方。
貼入一個Label,『Text』屬性設置為【新增數量】。
Ž 貼入一個InfoTextBox,命名為tbCount,用於User輸入需要新增多少筆資料。
最後貼入一個Button,命名為btInsert,『Text』屬性設為【Insert】。如圖所示:
圖 5-2-16 貼入元件
Step17>設定dgvData(InfoDataGridView)的『Data Source』為【ibsMaster】。
圖 5-2-17 設定dgvData Data Source
Step18>在btInsert的Click事件(點兩下btInsert元件)中寫下如下代碼:
private void
btInsert_Click(object
sender,
EventArgs e)
{
//呼叫S005
的MyInsert
方法,並將要新增的數量參數傳給Server
object[] back = CliUtils.CallMethod("S005", "MyInsert", new object[] {
tbCount.Text });
if (back[1].ToString() == "Y")
{ //如果回傳的參數為Y,表示成功,並將所有新增的資料顯示出來
Master.SetWhere("PurchaseID
in " +
back[2].ToString());
}
else //如果回傳失敗,則將失敗訊息Show給User
MessageBox.Show(back[2].ToString());
}
Step19>編譯C005,並在EEPManager中增加此功能項,然後以EEPNetClient來執行。
圖 5-2-18編譯C005
如下圖,在新增數量上輸入【3】,並按下「Insert」按鈕後,就會觸發A/P Server上執行S005.MyInsert程式,並讓Client可以顯示出剛才Insert的資料於Grid中。
圖 5-2-19 執行結果
q 在Server端進行資料整理
當有大量資料要進行統計與分析時,如果可以用一個單一的SQL語句即可取得結果時,則就可以直接由Client端的InfoDataSet透過InfoCommand來取得資料即可,但是,如果不能以單一的SQL語句直接取得資料,尚要在這些資料上進行大量的加工與整理時,就不能將資料下載到Client再進行處理,因為下載的資料也許是很大量的資料,會造成頻寬上的負荷,由於A/P Server往往與Database Server是在同一個網域當中,因此利用Server Method在A/P Server上執行是值得肯定的。以下我們舉例是為了統計某年度,各產品每月的銷售額。統計年度由User的Client端傳入,並在Server端做好統計後,將統計的結果回傳至Client端,並顯示出來。設計步驟如下:
Step1>用之前設計的S005,貼入一個InfoCommand,命名為Temp,作為統計要下SQL語句所使用。
圖 5-2-20 貼入InfoCommand元件
Step2>在ServiceManager中再添加一個ServerMethod,命名為OrderStatistic。
圖 5-2-21 貼入ServerMethod元件
Step3>將程式寫在MyInsert函式之下,程式碼如下:
public object OrderStatistic(object[] param)
{ object[] ret = new object[] { 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 (intj = 0; j < dtOrderData.Rows.Count; j++)
{ DataRow[] drs = dtOrder.Select("ProductID = \'" + dtOrderData.Rows[j][0].ToString() + "\'");
// 用目前這筆dtOrderData的PruductID去找dtOrder的ProductID
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 (inti = 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 Insert到dtOrder中
}
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。在C005的Form1,將原來TabControl的第二個TabPage的『Text』設為【OrderStatistic】。
圖 5-2-22/1 編譯S005項目
圖 5-2-22/2 設定TabPage2屬性
Step5>貼入元件。
Œ 在此Tab頁中增加一個InfoDataGridView,命名為dgvOrderStatistic,用於顯示統計結果。
再增加一個Label元件,Label的『Text』設為【統計年度:】。
Ž 再增加一個InfoTextBox元件,命名為tbYear,用於讓User輸入統計的年份。
最後增加一個Button元件,命名為btOrderStatistic,然後將Button的『Text』設為【Monthly
Report】。
圖 5-2-23 如圖貼入元件
Step6>點兩下btOrderStatistic這個Button,在Click事件中加入如下程式:
private void btOrderStatistic_Click(object sender, EventArgs
{
object[] back = CliUtils.CallMethod("S005", "OrderStatistic", new object[] { 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中,如下圖。
圖 5-2-24 執行
q 非同步Server Method設計
一般Client端呼叫Server Method都是用同步的方式呼叫,所謂同步,就是Client端呼叫後會去等待Server端的Method處理完畢並傳回值後才會讓Client繼續工作,大部份的Server Method不會執行過長的時間(如數秒內),因此Client等待是合理而且是可以接受的,但如果因為特殊的情況下,Server Method會執行很久(如數分鐘以上),此時如果讓Client端一直等待下去其實是不符合人性的,因此在EEP2010中我們另外提供了非同步的Server Method機制,讓Client呼叫後,可以不必等待執行結果,並可以繼續進行別的工作,等Server的Method執行完畢後,會另外通知Client端,來改善與突破傳統等待服務的缺失,我們同樣舉一個實例來說明,就是統計某段時間內,Order金額最多的10個客戶,並記錄這10個用戶分別購買最多的10個產品的總金額,顯示時我們要將客戶以橫向顯示(X軸),產品則以直向顯示(Y軸),如此可以看到客戶Order產品的交叉統計資料。
Step1>同樣用S005,在ServiceManager中增加一個ServerMethod,命名為CustStatistic。程式如下,請加在Component Designer generated code的上方:
public object CustStatistic(object[] param)
{ object[] ret = new object[] { 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語句來產生一個空的資料表結構, 來分別存放不同客戶所購買產品,其中N01到N10是代表第一個客戶到第十個客戶(橫向),最後會將此數據傳回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 = new DataTable();
// 依前十大客戶逐一處理
for (inti = 0; i < dtCust.Rows.Count; i++)
{
dtData.Columns[i + 2].ColumnName = dtCust.Rows[i][0].ToString();
// 將準備返回的dtData欄位名稱改成以客戶編號做為欄位名稱(這樣Client端的DataGridView的Header也會顯示此客戶編號), 從第個欄位開始改, 第,2個欄位原本為ProductID與ProductName
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" +"Where Orders. 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 (intj = 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。
圖 5-2-25 編譯S005項目
Step3>打開C005專案的Form1的設計畫面,在原來TabControl中添加一個TabPage,『Text』設為【CustStatistic】。
圖 5-2-26設定TabPage3屬性
Step4>貼入元件。
Œ 在此Tab頁中增加一個RichTextBox,用於顯示統計結果。
再貼入兩個InfoDateTimePicker,分別命名為dateFrom,dateTo。
Ž 以及兩個Label,第一個Label的『Text』設為【統計開始日期】,第二個Label的『Text』設為【統計結束日期】。
再貼入一個Button,『Name』設為btRunMethod,Text設為Run Method。
圖 5-2-27 貼入元件
Step5>在Button的Click事件中加入如下程式:
private void btRunMethod_Click(object sender, EventArgs e)
{
richTextBox1.Text = "";
CliUtils.AsyncCallMethod("S005", "CustStatistic", new object[] { dateFrom.Value, dateTo.Value }, MyCallBack);
//這是非同步的呼叫方式,因此還要定義MyCallBack
}
非同步呼叫與同步呼叫是不同,同步呼叫需等待Server端執行完成,而非同步呼叫無需等待,在Server端執行Method完成後,才會通知Client,而通知的方式就是上例中的MyCallBack,一個自定義的函數,就是指Server端執行完畢後所要通知Client的程式進入點。所以下列是MyCallBack的程式,如下:(可以加在 private void btRunMethod_Click(object sender, EventArgs e)上方)
public void 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上。
圖 5-2-28 執行結果
Converted from CHM to HTML with chm2web Pro 2.85 (unicode) |