專刊內文

當前位置:首頁>專刊分享>內文

瀏覽次數 : 702



開放架構的EEP MVC模組

訊光科技/林蕙君

前言

        EEP這個框架源自2000年訊光在Delphi時代的構想,為了能讓多數企業的IT部門及從事商用開發的IT業者,有一個開放性的元件化平台,共享元件、共享架構、共享開發經驗,讓軟體開發工作可以被高度重複使用及共享共用。沒想到一路走來,都快20年了,從Delphi.Net平台,提供了無數的版本與解決方案。開放架構一直是我們的精神與目標,但始終遺憾的就是平台的基礎架構與基礎資料表綑綁過緊,許多企業就不得不被我們平台的系統資料表所限制住而增生困擾。

 

        這個問題難在,如何提供一個立即可用又能配合企業自己的基礎資料表呢? 自從EEP投入了MVC研發之後,透過ModelView的高度不耦合特性得到解決方案。透過EEP MVC架構即可非常容易抽換成自己的基礎資料表,不必再使用EEP內定的系統資料表,而且所有EEP的功能與架構完全不受影響,從此讓EEP在開放性的架構上又更上層樓。

 

架構

        EEP MVC提供了一個重要的模組MVCTools,內容主要有MVCService提供一些共用接口,來讓EEP與你自己的系統表共同整合;另一個就是MVCDataObject,此用來包裝來自EEP A/P Server資料表給你所設計的View來使用,並針對資料新增/更改/刪除/查詢等動作會自動對應到後端的A/P Server上。

 

如圖,EEP MVC寫好了一些基礎框架,如登入、主畫面、忘記密碼與更改、使用者或群組管理、權限設定等等的ViewController,都會以MVCTools的共用接口(IGroupServiceIUserServiceIMenuSericeIAccountService)來讀取或回寫資料。其中共有三種管道來存取這些系統資料與資源,如下:

 

1. 原生EEP的方式:使用MVCToolsMVCService這些接口與EEP傳統的GLModule對接,此方式當然會去讀取EEP標準的系統資料表(USERSGROUPSTable)

 

2. 透過EEP A/P Server:如果你使用的系統資料表與EEP不同,也可以透過EEPA/P Server來存取自己的系統表,當然你必須透過MVCToolsMVCDataObject做為橋樑並掛接MVCService的這些IService接口往下開發。

 

3. 不透過EEP A/P Server:如果你自己的系統資料表,不想透過EEPA/P Server來處理,也可以直接開發一個模組取代,當然你自己的資料模型(Model)需透過EDMX來建立,並掛接MVCService的這些IService接口進行實例開發。

 

以上的好處是,不管你使用哪一種方式,這些共用的系統頁面格式View(cshtml)Controller都完全不必更改,這也是MVC架構所帶來的好處。


擷取.JPG

 

接著,我們針對第2與第3的方式來舉例說明,在EEP中如何抽換自己的系統資料表。

 

 

透過EEP A/P Server

        這個案例中,我們假設EEP開發者有自己的USERSGROUPSUSERGROUPS等系統資料表,使用者表名為Test、群組表名為TestGroups、群組明細表名為TestUserGroups等。我們可以在VS打開EEPMVC方案,在MVCTools專案之下打開MVCService.cs,可以看到有以下的IGroupServiceIUserServiceIMenuSericeIAccountServiceService接口。

開發步驟如下:

1.      新增一個專案,選擇C#裡面的類別庫,並定義一個類別名稱,如:MyAccount,我們要透過這個專案來實作上面的Service接口。

 

2.      MVCTools專案加入MyAccount的參考。

 

3.      可以透過EEP A/P Server來開發你自己的USERS等系統表,使用EEPWizard做一個ServerPacketge 命名為sTEST,選入自定義的三個表作為資料來源(分別為TestTestGroupsTestUserGroups)

 

4.      MyAccount下面,新增一個UserService.csUserService class,來對應Test這個使用者表(取代EEPUSERS),繼承接口為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.csGroupService class,來對應TestGroup這個群組表(取代EEPGROUPS),程式就與上面的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 =MVCToolsInterface的類別接口,type = 自行定義的Interface接口類別,逗號後面為DLL模組名稱;如name="IUserService" Type="MyAccount.UserService,MyAccount",代表IUserServicve的接口由MyAccountUserService接口取代之;name="IMenuService" Type="MVCTools,menuService,MVCTools",代表IMenuSercie使用EEP原生的Menu選單系統資料表(MENUTABLE),而非自行設計的Menu資料表等。

 

9.      接著,可以不必更動所有的ControllerView,即可抽換成自己的使用者與群組資料表,先在Test這個User表中,建立一筆TestID"001"的帳號,並將Type設為'S'代表為系統管理者。

 

10.  EEP中,我們可以在MVCWebClient網站中找到View\System\Logon這個index的登入網頁,使用"在瀏覽器中檢視"來打開登入頁面,並以"001"登入。

 

11.  登入後,可以直接在網址後面加上/menu進行EEP系統表單的掛載,如圖,我們可以將View\System\UserGroupindex頁面掛入選單中,並設定權限。

 

12.  設定完重新登入或F5更新主畫面,就可以打開UserGroup這兩個頁面,並可以管理系統的使用者與群組,你所輸入的用戶資料當然會存到你自己的系統表TestTestGroupsTestUserGroups中。如圖:

User

Group

 

 

不透過EEP A/P Server

        接著的案例,我們將不透過EEP A/P Server來開發自己的USERSGROUPSUSERGROUPS等系統資料表,也就是上文中的第三種方式存取系統資料;表名我們用了另外一個客製ERP的使用者表為TestUser、群組表為TestGroup、群組明細表為UserInGroups(結構於下文中)

 

開發步驟如下

1.      新增一個專案,選擇C#裡面的類別庫,並定義一個類別名稱,如:TestAccount

 

2.      MVCTools專案加入參考。

 

3.      TestAccount上面新增一個資料夾,命名為「Models」,用來存放自己的實體資料模型。

 

4.      Models資料夾裡新增一個新項目,建立「ADO.NET實體資料模型」(EDMX),命名為TestEntities

完成後如圖所示:(透過EDMX大約可了解資料表結構與EEP的系統表不同)

 

 

5.      TestAccount下面,新增一個TestUserService.cs與對應的Class,繼承的接口為IUserService,程式如下:

 

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這個群組表(取代EEPGROUPS),程式就與上面的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/GroupsInterface接口,如下:

 

 

上面的IAccountServiceIUserServiceIGroupService類別接口都是對應到我們自行開發的TestAccount中,只有IMenuService我們還是沿用EEP原生的Menu選單系統資料表,而非自行設計的Menu資料表等。

 

10.  接著,已經完成了我們自定義的系統資料表於EEP中來使用,不必更動所有的ControllerView,同樣先在TestUser這個表中,建立一筆TestID"001"的帳號,並將Type設為'S'代表為系統管理者。

 

11.  EEP中,我們可以在MVCWebClient網站中找到View\System\Logon這個index的登入網頁,使用"在瀏覽器中檢視"來打開登入頁面,並以"001"登入。登入後,可以直接在網址後面加上/menu進行EEP系統表單的掛載,如圖,我們可以將View\System\UserGroupindex頁面掛入選單中,並設定權限。

 

 

此方式雖然可以不必透過EEP A/P Server可以自由發揮,系統的登入/登出及表單權限等都由你自由控制,但在RuntimeIAccountService接口之後,為了集中管理A/P Server狀態,包括Log那些User登入/登出強制踢除User、管理Pool連線數等等,都是A/P Server所必須負責的事,因此在IAccountServer動作後還是會與A/P Server交互訊息達到集中管理的目的。

 

 

結論

        EEP MVC提供了常用且標準的頁面模組,包括Home首頁、用戶Login、忘記或變更密碼、功能表權限、用戶或群組管理、權限設定、多國語言管理、錯誤例外管理、日誌管理等,除了View頁面外還有對應的Controller。透過MVCTools模組的接口(interface),可整合其他系統的資源,如單一登入、使用者、群組(角色)、組織、權限等資源。來面對未來的需求,EEPMVC的新技術來開放這些核心架構,讓EEP的開發者不但享有EEP便捷快速開發的能力,又能兼顧彈性與整合能力,相信對EEP的框架而言,又向前邁進了一大步。