 
      
    
開放架構的EEP 
MVC模組
訊光科技/林蕙君
前言
       
EEP這個框架源自2000年訊光在Delphi時代的構想,為了能讓多數企業的IT部門及從事商用開發的IT業者,有一個開放性的元件化平台,共享元件、共享架構、共享開發經驗,讓軟體開發工作可以被高度重複使用及共享共用。沒想到一路走來,都快20年了,從Delphi到.Net平台,提供了無數的版本與解決方案。開放架構一直是我們的精神與目標,但始終遺憾的就是平台的基礎架構與基礎資料表綑綁過緊,許多企業就不得不被我們平台的系統資料表所限制住而增生困擾。
       
這個問題難在,如何提供一個立即可用又能配合企業自己的基礎資料表呢? 
自從EEP投入了MVC研發之後,透過Model與View的高度不耦合特性得到解決方案。透過EEP 
MVC架構即可非常容易抽換成自己的基礎資料表,不必再使用EEP內定的系統資料表,而且所有EEP的功能與架構完全不受影響,從此讓EEP在開放性的架構上又更上層樓。
架構
EEP MVC提供了一個重要的模組MVCTools,內容主要有MVCService提供一些共用接口,來讓EEP與你自己的系統表共同整合;另一個就是MVCDataObject,此用來包裝來自EEP A/P Server資料表給你所設計的View來使用,並針對資料新增/更改/刪除/查詢等動作會自動對應到後端的A/P Server上。
如圖,EEP MVC寫好了一些基礎框架,如登入、主畫面、忘記密碼與更改、使用者或群組管理、權限設定等等的View與Controller,都會以MVCTools的共用接口(如IGroupService、IUserService、IMenuSerice、IAccountService)來讀取或回寫資料。其中共有三種管道來存取這些系統資料與資源,如下:
1. 原生EEP的方式:使用MVCTools的MVCService這些接口與EEP傳統的GLModule對接,此方式當然會去讀取EEP標準的系統資料表(如USERS、GROUPS等Table)。
2. 透過EEP A/P Server:如果你使用的系統資料表與EEP不同,也可以透過EEP的A/P Server來存取自己的系統表,當然你必須透過MVCTools的MVCDataObject做為橋樑並掛接MVCService的這些IService接口往下開發。
3. 不透過EEP A/P Server:如果你自己的系統資料表,不想透過EEP的A/P Server來處理,也可以直接開發一個模組取代,當然你自己的資料模型(Model)需透過EDMX來建立,並掛接MVCService的這些IService接口進行實例開發。
以上的好處是,不管你使用哪一種方式,這些共用的系統頁面格式View(cshtml)與Controller都完全不必更改,這也是MVC架構所帶來的好處。
接著,我們針對第2與第3的方式來舉例說明,在EEP中如何抽換自己的系統資料表。
透過EEP 
A/P Server
這個案例中,我們假設EEP開發者有自己的USERS與GROUPS、USERGROUPS等系統資料表,使用者表名為Test、群組表名為TestGroups、群組明細表名為TestUserGroups等。我們可以在VS打開EEP的MVC方案,在MVCTools專案之下打開MVCService.cs,可以看到有以下的IGroupService、IUserService、IMenuSerice、IAccountService等Service接口。
開發步驟如下:
1. 新增一個專案,選擇C#裡面的類別庫,並定義一個類別名稱,如:MyAccount,我們要透過這個專案來實作上面的Service接口。
2. 把MVCTools專案加入MyAccount的參考。
3. 可以透過EEP A/P Server來開發你自己的USERS等系統表,使用EEPWizard做一個ServerPacketge 命名為sTEST,選入自定義的三個表作為資料來源(分別為Test、TestGroups及TestUserGroups)。
4. 在MyAccount下面,新增一個UserService.cs及UserService的 class,來對應Test這個使用者表(取代EEP的USERS表),繼承接口為IUserService,如下程式:
namespace MyAccount
{   
public class UserService : MVCTools.IUserService
   
{   private MVCDataObject 
UserObject  //取得用戶資料來源
       
{   get
           
{   return new 
MVCDataObject() { Module = "sTEST", Command = "Test" };
           
}
       
}
       
public void Add(UserDetailViewModel model) 
//新增用戶資料
       
{   
UserObject.InsertRow(new Dictionary<string, object> { { "TestID", model.ID},           
{ "TestName", model.Name}, { "Email", model.Email}, { "Type", 
UserTypeToString(model.Type) } });
       
}
        public 
IEnumerable<UserViewModel> Get()  //取得所有用戶資料
       
{   return 
UserObject.GetDataTable().Rows.OfType<DataRow>().Select(row => 
DataRowToUserModel<UserViewModel>(row));
       
}
       
public UserDetailViewModel Get(string id) 
//取得單一用戶資料
    
    {   var userObject = 
UserObject;
           
userObject.WhereStr = $"TestID = '{id.Replace("'", "''")}'";
           
return 
DataRowToUserModel<UserDetailViewModel>(userObject.GetDataTable().Rows.OfType<DataRow>().FirstOrDefault());
       
}
       
//將用戶資料寫入模組中
       
private T DataRowToUserModel<T>(DataRow row) where T : UserViewModel
       
{   if (row != null)
           
{   dynamic model = 
typeof(T).GetConstructor(new Type[] { }).Invoke(null);
               
model.ID = row["TestID"].ToString();
               
model.Name = row["TestName"].ToString();
               
model.Email = row["Email"].ToString();
               
model.Type = StringToUserType(row["Type"].ToString());
               
return (T)model;
           
}
           
else {   return null;
           
}
       
}
       
public void Remove(string id)  
//刪除用戶資料
       
{   
UserObject.DeleteRow(new Dictionary<string, object> { { "TestID", id} });
       
}
       
public void Update(UserDetailViewModel model) 
//更改用戶資料
       
{   
UserObject.UpdateRow(new Dictionary<string, object> { { "TestID", model.ID},              
{ "TestName", model.Name}, { "Email", model.Email}, { "Type", 
UserTypeToString(model.Type) } });
       
}
       
private UserType StringToUserType(string value) 
//使用者類型型別轉換
       
{   if (value?.ToUpper() 
== "S")
           
{   return 
UserType.Admin;
           
}
           
else if (value?.ToUpper() == "X")
           
{   return 
UserType.Disabled;
           
}
           
else
           
{   return UserType.User;
           
}
       
}
       
//使用者資料轉換為使用者類型
       
private string UserTypeToString(UserType type) 
//使用者資料轉換轉換
       
{   switch (type)
           
{   case UserType.Admin: 
return "S";
               
case UserType.Disabled: return "X";
      
          default: return "U";
           
}
       
}
   
}
}
5. 同樣在MyAccount下面,新增一個GroupService.cs及GroupService的 class,來對應TestGroup這個群組表(取代EEP的GROUPS表),程式就與上面的UserService.cs差不多,只是將Test改用TestGroups資料表而已,繼承接口為IGroupService,不再贅述。
6. 再新增一個cs檔,來設定登入驗證與主畫面的一些標準功能,繼承接口為IAccountService,我們將其命名為AccountService.cs,如下的程式:
namespace MyAccount
{   
public class AccountService : IAccountService
   
{   public string SSOKey 
//取得單一嵌入的認證號碼
       
{   get
           
{   return "infolight"; 
//假設為"infolight",傳回EEPNetServer-->Server Config裡的SSO Key
           
}
       
}
       
public string License  //取得註冊訊息
       
{   get
           
{   return "060103N0 + WF 
+ M"; //傳回EEPNetServer上的註冊序號
           
}
       
}
       
       
private SqlConnection CreateConnection()  
//取得資料庫連線
       
{   var builder = new 
SqlConnectionStringBuilder()
           
{   DataSource = ".", 
InitialCatalog = "Northwind", UserID = "rena", Password = "123" };
           
return new SqlConnection(builder.ToString());
       
}
       
public LogonResult Login(LoginViewModel model)  
//實作登入驗證
       
{   using (var connection 
= CreateConnection())
           
{   connection.Open();
               
var command = connection.CreateCommand();
               
command.CommandText = ($"Select * from Test where TestID = 
'{model.LogonName}'");
               
var reader = command.ExecuteReader();
               
if (reader.Read())
               
{   if (model.Password == 
(string)reader["PWD"])
                   
{   return 
LogonResult.Logoned;
                   
}
               
}
               
return LogonResult.PasswordError;
           
}
       
}
       
public IEnumerable<GroupInfo> GetGroups(string user) 
//取得登入後的群組資料
       
{   using (var connection 
= CreateConnection())
           
{   connection.Open();
               
var command = connection.CreateCommand();
               
command.CommandText = ($"select TestUserGroups.* from TestUserGroups 
where USERID = '{user}'");
               
var adapter = new SqlDataAdapter(command);
               
var dataTable = new DataTable();
               
adapter.Fill(dataTable);
               
return dataTable.Rows.OfType<DataRow>().Select(row => new 
MVCTools.WCF.GroupInfo() { ID = row["GROUPID"].ToString() });
           
}
       
}
       
public string GetUserName(string user) 
//取得用戶名稱
       
{   using (var connection 
= CreateConnection())
           
{   connection.Open();
               
var command = connection.CreateCommand();
               
command.CommandText = ($"Select * from Test where TestID = '{user}'");
               
var reader = command.ExecuteReader();
               
return reader.Read() ? (string)reader["TestName"] : string.Empty;
           
}          
       
}
       
public bool CheckRight(string user, string controller) 
//取得權限驗證
       
{   var userObject = 
UserObject;
           
userObject.WhereStr = $"TestID = '{user.Replace("'", "''")}'";
           
var row = 
userObject.GetDataTable().Rows.OfType<DataRow>().FirstOrDefault();
           
if (row != null)
           
{   return 
row["Type"].ToString().ToUpper() == "S";
           
}
           
return false;
       
}
       
private MVCDataObject UserObject 
//取得用戶資料來源
       
{   get
           
{   return new 
MVCDataObject() { Module = "sTEST", Command = "Test" };
           
}
       
}
       
private MVCDataObject GroupUserObject 
//取得群組資料來源
       
{   get
           
{   return new 
MVCDataObject() { Module = "sTEST", Command = "TestUserGroups" };
           
}
       
}
   
}
}
7. 在MVCWebClient網站裡把自己設計的MyAccount類別加入參考。
8. MVCWebClient\Web.config裡,在<mvcServer><services>下定義自己所開發的Interface,如下:
name =為MVCTools中Interface的類別接口,type = 自行定義的Interface接口類別,逗號後面為DLL模組名稱;如name="IUserService" Type="MyAccount.UserService,MyAccount",代表IUserServicve的接口由MyAccount的UserService接口取代之;name="IMenuService" Type="MVCTools,menuService,MVCTools",代表IMenuSercie使用EEP原生的Menu選單系統資料表(如MENUTABLE),而非自行設計的Menu資料表等。
9. 接著,可以不必更動所有的Controller與View,即可抽換成自己的使用者與群組資料表,先在Test這個User表中,建立一筆TestID為"001"的帳號,並將Type設為'S'代表為系統管理者。
10. 在EEP中,我們可以在MVCWebClient網站中找到View\System\Logon這個index的登入網頁,使用"在瀏覽器中檢視"來打開登入頁面,並以"001"登入。
11. 登入後,可以直接在網址後面加上/menu進行EEP系統表單的掛載,如圖,我們可以將View\System\User及Group的index頁面掛入選單中,並設定權限。
12. 設定完重新登入或F5更新主畫面,就可以打開User及Group這兩個頁面,並可以管理系統的使用者與群組,你所輸入的用戶資料當然會存到你自己的系統表Test、TestGroups與TestUserGroups中。如圖:
User:
Group:
不透過EEP 
A/P Server
        接著的案例,我們將不透過EEP 
A/P Server來開發自己的USERS與GROUPS、USERGROUPS等系統資料表,也就是上文中的第三種方式存取系統資料;表名我們用了另外一個客製ERP的使用者表為TestUser、群組表為TestGroup、群組明細表為UserInGroups等(結構於下文中)。
開發步驟如下:
1. 新增一個專案,選擇C#裡面的類別庫,並定義一個類別名稱,如:TestAccount。
2. 把MVCTools專案加入參考。
3. 在TestAccount上面新增一個資料夾,命名為「Models」,用來存放自己的實體資料模型。
4. 在Models資料夾裡新增一個新項目,建立「ADO.NET實體資料模型」(EDMX),命名為TestEntities。
完成後如圖所示:(透過EDMX大約可了解資料表結構與EEP的系統表不同)
namespace TestAccount
{   
public class TestUserService : IUserService //引用自己的Service
   
{   public 
UserDetailViewModel Get(string id)  
//取得單一用戶資料
       
{   using (var entities = 
new TestEntities())
           
{   UserDetailViewModel 
user = null;
               
var userEntity = entities.TestUser.FirstOrDefault(u => u.TestID == id);
               
if (userEntity != null)
               
{   user = 
GetTargetUserDetail(userEntity);
                   
user.Type = StringToUserType(userEntity.Type);
               
}
               
return user;
           
}
       
}
       
public IEnumerable<UserViewModel> Get() 
//取得所有用戶資料
       
{   using (var entities = 
new TestEntities())
           
{   
IEnumerable<UserViewModel> users = new List<UserViewModel>();
          
      var userEntities = entities.TestUser.ToList();
               
if (userEntities != null)
                   
users = GetTargetUserList(userEntities);
               
return users;
           
}
       
}
       
public void Add(UserDetailViewModel user)  
//新增用戶資料
       
{   using (var entities = 
new TestEntities())
           
{   var existUser = 
entities.TestUser.FirstOrDefault(u => u.TestID == user.ID);
               
if (existUser == null)
               
{   var u = new 
TestUser()
             
       {   TestID = 
user.ID, TestName = user.Name, Email = user.Email, Type = 
UserTypeToString(user.Type) };
                   
entities.TestUser.Add(u);
                   
entities.SaveChanges();
               
}
           
}
       
}
       
public void Update(UserDetailViewModel user) 
//更改用戶資料
       
{   using (var entities = 
new TestEntities())
           
{   var existUser = 
entities.TestUser.FirstOrDefault(u => u.TestID == user.ID);
               
if (existUser != null)
               
{   existUser.TestID = 
user.ID;
                   
existUser.TestName = user.Name;
                   
existUser.Email = user.Email;
                   
existUser.Type = UserTypeToString(user.Type);
                   
entities.Entry(existUser).State = 
System.Data.Entity.EntityState.Modified;
                   
entities.SaveChanges();
               
}
           
}
       
}
       
public void Remove(string id)  
//刪除用戶資料
       
{   using (var entities = 
new TestEntities())
           
{   var existUser = 
entities.TestUser.FirstOrDefault(u => u.TestID == id);
               
if (existUser != null)
               
{   
entities.TestUser.Remove(existUser);
                   
entities.SaveChanges();
               
}
           
}
       
}
       
private MVCTools.Models.UserType StringToUserType(string value) 
//使用者類型型別轉換為資料
       
{   if (value?.ToUpper() 
== "S")
           
{   return 
MVCTools.Models.UserType.Admin;
           
}
           
else if (value?.ToUpper() == "X")
           
{   return 
MVCTools.Models.UserType.Disabled;
           
}
           
else
           
{   return 
MVCTools.Models.UserType.User;
           
}
       
}
       
private string UserTypeToString(MVCTools.Models.UserType type) 
//使用者資料轉換為使用者類型
       
{   switch (type)
      
      {   case 
MVCTools.Models.UserType.Admin: return "S";
               
case MVCTools.Models.UserType.Disabled: return "X";
               
default: return "U";
           
}
       
}
       
private UserDetailViewModel GetTargetUserDetail(TestUser model) 
//取得單一用戶資料
       
{   var user = new 
UserDetailViewModel()
           
{   ID = model.TestID, 
Name = model.TestName, Email = model.Email, Type = StringToUserType(model.Type) 
};
           
return user;
       
}
       
private IEnumerable<UserViewModel> GetTargetUserList(List<TestUser> 
model)  //取得所有用戶資料
       
{   using (var entities = 
new TestEntities())
           
{   
IEnumerable<UserViewModel> users = new List<UserViewModel>();
               
users = (from g in model select new UserViewModel()
                        
{   ID = g.TestID, Name = 
g.TestName, Email = g.Email, Type = StringToUserType(g.Type) }).ToList();
               
return users;
           
}
       
}
   
}
}
6. 同樣在TestAccount下面,新增一個TestGroupService.cs與其 class,來對應TestGroup這個群組表(取代EEP的GROUPS表),程式就與上面的TestUserService差不多,只是將TestUser改用TestGroup資料表而已,繼承接口為IGroupService,不再贅述。
7. 在TestAccount新增一個cs檔,實作登入驗證與主畫面共用功能,並命名為TestAccountService.cs,參考範例程式:
namespace TestAccount
{   
public class TestAccountService : IAccountService //引用自己的Service
    {  
public string SSOKey
       
{  get
           
{   return "infolight"; 
//假設為"infolight",傳回EEPNetServer-->Server Config裡的SSO Key
           
}
       
public string License  //註冊訊息
       
{   get
           
{   return "060103N0 + WF 
+ M";  //傳回EEPNetServer上的註冊序號
           
}
       
}
       
public LogonResult Login(LoginViewModel model) 
//登入驗證
       
{   using (var entities = 
new TestEntities())
           
{   var existUser = 
entities.TestUser.FirstOrDefault(n => n.TestID == model.LogonName /*&& n.PWD == 
model.Password*/);
               
if (existUser != null)
                   
return LogonResult.Logoned;
               
return LogonResult.PasswordError;
           
}
       
}
       
public IEnumerable<GroupInfo> GetGroups(string user) 
// 
取得群組資料
       
{   using (var entities = 
new TestEntities())
           
{   
IEnumerable<GroupInfo> userList = new List<GroupInfo>();
               
var existUser = entities.UserInGroup.Where(uig => uig.USERID == 
user).ToList();
       
         if (existUser != null)
               
{   userList = (from u in 
existUser select new GroupInfo()
                               
{   ID = u.GROUPID, Name 
= u.USERID }).ToList();
               
}
               
return userList;
           
}
        }
       
public string GetUserName(string user) 
//取得用戶名稱
       
{   using (var entities = 
new TestEntities())
           
{   return 
entities.TestUser.FirstOrDefault(u => u.TestID == user).TestName ?? 
string.Empty;
           
}
       
}
       
public bool CheckRight(string user, string controller) 
//權限驗證
       
{   using (var entities = 
new TestEntities())
           
{   var existUser = 
entities.TestUser.FirstOrDefault(u => u.TestID == user);
               
if (existUser != null)
           
         return existUser.Type.ToUpper() == "S";
               
return false;
           
}
       
} 
   
}
}
8. 在MVCWebClient網站上,把TestAccount類別加入參考。
9. MVCWebClient\Web.config裡,在<mvcServer><services>下,定義自己的Account/Users/Groups的Interface接口,如下:
上面的IAccountService、IUserService與IGroupService類別接口都是對應到我們自行開發的TestAccount中,只有IMenuService我們還是沿用EEP原生的Menu選單系統資料表,而非自行設計的Menu資料表等。
10. 接著,已經完成了我們自定義的系統資料表於EEP中來使用,不必更動所有的Controller與View,同樣先在TestUser這個表中,建立一筆TestID為"001"的帳號,並將Type設為'S'代表為系統管理者。
11. 在EEP中,我們可以在MVCWebClient網站中找到View\System\Logon這個index的登入網頁,使用"在瀏覽器中檢視"來打開登入頁面,並以"001"登入。登入後,可以直接在網址後面加上/menu進行EEP系統表單的掛載,如圖,我們可以將View\System\User及Group的index頁面掛入選單中,並設定權限。
此方式雖然可以不必透過EEP 
A/P Server可以自由發揮,系統的登入/登出及表單權限等都由你自由控制,但在Runtime的IAccountService接口之後,為了集中管理A/P 
Server狀態,包括Log那些User登入/登出、強制踢除User、管理Pool連線數等等,都是A/P 
Server所必須負責的事,因此在IAccountServer動作後還是會與A/P 
Server交互訊息達到集中管理的目的。
結論
EEP MVC提供了常用且標準的頁面模組,包括Home首頁、用戶Login、忘記或變更密碼、功能表權限、用戶或群組管理、權限設定、多國語言管理、錯誤例外管理、日誌管理等,除了View頁面外還有對應的Controller。透過MVCTools模組的接口(interface),可整合其他系統的資源,如單一登入、使用者、群組(角色)、組織、權限等資源。來面對未來的需求,EEP以MVC的新技術來開放這些核心架構,讓EEP的開發者不但享有EEP便捷快速開發的能力,又能兼顧彈性與整合能力,相信對EEP的框架而言,又向前邁進了一大步。