開放架構的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的框架而言,又向前邁進了一大步。