2015年2月10日 星期二

MVC進階體驗-範例流程表單

    因為公司內部的需求,需要有表單跑流程,作者以Asp.net MVC+Angular.js 快速開發有流程需求單,軟體架構是分層方式(控制層、實體層),將控制邏輯的程式碼放在控制層,把與資料的相關函式(儲存、修改、查詢資料)列在實體層,如此單一職責,更方便測試每一個函式邏輯。

一、    系統需求


流程圖


流程說明
1.    申請人: 所有人員需填寫申請單, 並發出MAIL通知
2.    申請人的主管: 負責核可/退回案件申請
3.    RD窗口: 選擇處理人員,再MAIL 通知處理人員
4.    處理人員:
l   同意à填寫完工日
l   不同意à填寫不同意的原因à發出Mail通知主管、申請人
5.    確認完工: 發出MAIL通知主管、申請人
6.    流程表單
l   流程1: 申請人à主管à RD窗口à負責處理人à完工確認
l   流程2: 申請人(主管)à RD窗口à負責處理人à完工確認
類別圖

    根據分層架構邏輯,作者分成控制層(controller)、實體實(entity),職責分離,再由asp.net MVC Controller 呼叫控制層類別


一、    實體層:單一職責

l   listRequests(): 列出所有表單的語法

    public IEnumerable<entityRequest> listRequests(string applyUserID, DateTime? startDate, DateTime? endDate)
        {
                var query = from p1 in tcsDB.applyRequests
                  join p2 in tcsDB.alluser on p1.applyUser equals p2.uid into userName
                  join p3 in tcsDB.alluser on p1.processUser equals p3.uid into flowUser
                  join p4 in tcsDB.applyRequestsAuthorization on p1.applyUser equals p4.userID into authorizeUser
                            from apply in userName.DefaultIfEmpty()
                            from process in flowUser.DefaultIfEmpty()
                            from authorize in authorizeUser.DefaultIfEmpty()
                            where (p1.applyUser == applyUserID || authorize.bossID == applyUserID)
                            orderby p1.requestDate descending
                             select new entityRequest
                            {
                                ikey = p1.ikey,
                                applyUserName = apply.name,
                                applyType = p1.applyType,
                                requestDate = p1.requestDate,
                                applyReason = p1.applyReason,
                                processName = p1.processName,
                                processFlow = p1.processFlow,
                                processUser = process.name,
                                accepTaskUser = p1.accepTaskUser,
                                processAction = p1.processAction
                            };
////設定起迄區間,可以後續增加where 條件
                if (startDate != null && endDate != null)
                {
                    query = query.Where(m => m.requestDate >= startDate && m.requestDate <= endDate);
                }
                var results = query.Select(n => new entityRequest
                {
                    ikey = n.ikey,
                    applyUserName = n.applyUserName,
                    applyType = n.applyType,
                    requestDate = n.requestDate,
                    applyReason = n.applyReason,
                    processName = n.processName,
                    processFlow = n.processFlow,
                    processUser = n.processUser,
                    accepTaskUser = n.accepTaskUser,
                    processAction = n.processAction,
                }).AsEnumerable();
                return results.AsEnumerable<entityRequest>();
            }
          }

l   doNextFlow (): 跳下一個流程
  public void doNextFlow(string strProcessType, int intApplyRequestID, DateTime? dTimePredictCompleteDate)
        {          
                var applyRequestObj = tcsDB.applyRequests.Find(intApplyRequestID);
                FlowItem nextFlowItem = entityProcessFlowObj.getFlowNext(strProcessType, applyRequestObj.processFlow); //找出下一個流程
                if (nextFlowItem == null)
                {
                    return;
                }
                applyRequestObj.processFlow = nextFlowItem.flowItemID; //下一個流程ID
                applyRequestObj.processName = nextFlowItem.flowItemName.Trim();//下一個流程名稱
                applyRequestObj.processAction = nextFlowItem.flowRoleFunc.Trim();//下一個流程動作
                //儲存預期日期
                if (dTimePredictCompleteDate != null)
                {
                    applyRequestObj.predictCompleteDate = dTimePredictCompleteDate;
                }
                entityFlowRole entityFlowRoleUser = new entityFlowRole();
                flowRole flowRoleUser = entityFlowRoleUser.getUserRole(applyRequestObj.ikey, applyRequestObj.applyUser, nextFlowItem.flowRoleFunc); //找出下一流程的處理者
                applyRequestObj.processUser = flowRoleUser.strRoleUserID.Trim();               
                tcsDB.SaveChanges();
            }
          }

l   相關動作處理行為
//同意
public  void  agree(string strProcessType, int intApplyRequestID)
        {
            doNextFlow(strProcessType, intApplyRequestID,null);//跑下一流程
        }
//不准
public  void  reject(string strProcessType, int intApplyRequestID, string strRejectReason)
        {          
                applyRequests applyRequestObj = tcsDB.applyRequests.Where(m => m.ikey == intApplyRequestID).FirstOrDefault();
                applyRequestObj.processAction = "reject";
                applyRequestObj.rejectReason = strRejectReason; //退回的理由
                applyRequestObj.completeDate = DateTime.Now;
                tcsDB.SaveChanges();
            }
//RD窗口
public void rdDispatch(string strProcessType, int intApplyRequestID,  string strRdAcceptTaskUserID)
        {          
                applyRequests applyRequestObj = tcsDB.applyRequests.Where(m => m.ikey == intApplyRequestID).FirstOrDefault();
                applyRequestObj.accepTaskUser = strRdAcceptTaskUserID;
                applyRequestObj.processAction = "rdDispatch";
                tcsDB.SaveChanges();
                doNextFlow(strProcessType, intApplyRequestID,null);         
        }
//完工
  public void complete(string strProcessType, int intApplyRequestID)
        {           
                applyRequests applyRequestObj = tcsDB.applyRequests.Where(m => m.ikey == intApplyRequestID).FirstOrDefault();
                applyRequestObj.processAction = "end";
                applyRequestObj.processName = "結束";
                applyRequestObj.completeDate = DateTime.Now;
                tcsDB.SaveChanges();
            }
        }


l   角色設定: 依據角色定義,傳回該角色的相關屬性
     public flowRole getUserRole(int intRequestID,string strApplyUserID, string strRoleName)
        {
                switch (strRoleName)
                {
                    case "boss":
                        return boss(strApplyUserID);
                    case "rdDispatch":
                        return rdDispatchRole();
                    case "rdAcceptTaskUser":
                        return rdAcceptTaskUser(intRequestID);
                    default:
                        return rdAcceptTaskUser(intRequestID);
                }
            }
        }

二、    Linq相關範例

l  Left Join
var query = from p1 in tcsDB.applyRequestsAuthorization
          join p2 in tcsDB.alluser on p1.userID equals p2.uid into roleUserName
          from role in roleUserName.DefaultIfEmpty()
          where p1.department == "3"
          select new flowRole
                            {
                                strRoleUserID = role.uid,
                                strRoleUserName = role.name,
                                strProcessName = "RD處理人員"
                            };

l  Union
var query = (from p1 in tcsDB.alluser
      where p1.uid == userID
      select new flowRole
      {
             strRoleUserID = p1.uid,
            strRoleUserName = p1.name,
          }).Union(
            from p2 in tcsDB.applyRequestsAuthorization
            join p3 in tcsDB.alluser on p2.userID equals p3.uid into users
            from user in users.DefaultIfEmpty()
            where p2.bossID == userID
             select new flowRole
                            {
                                strRoleUserID = user.uid,
                                strRoleUserName = user.name
                            });

三、     Angular 相關應用
本系統的簽核的畫面,將共同的畫面,獨立寫成一隻html檔,例如申請人的相關訊息(申請日期、申請理由、申請人)等等,再使用Angular Directive 呼叫共同畫面(html檔案)

l   Directive: 範例 :

angular
.directive("signViewTemplate", function () {
         return {
             templateUrl: 'signViewTemplate'
         }
     })

asp.net mvc controller
public ActionResult signViewTemplate()
        {           
                int intApplyRequestID =(int) Session["intApplyRequestID"];
                entityRequest entityRequestData = new entityRequest(); //取得實體層傳回的資料
                entityRequestData = controlObj.readApplyRequest(intApplyRequestID);
                return View(entityRequestData);
         }

檔名: SignViewTemplate.cshtml
    <div class="row">
        <div class="col-md-1">需求日期  </div>
        <div class="col-md-4">
            @Html.ValueFor(m => m.requestDate)
        </div>
    </div>
    <div class="row">
        <div class="col-md-1">申請理由</div>
        <div class="col-md-4">
            @Html.Raw(Model.applyReason)
        </div>
    </div>
<div class="row">
    <button onclick="window.history.go(-1)">返回上一頁</button>
</div>

檔名: SignViewTemplate.cshtml
@model applyRequests.Models.alluser
<br />
@using (Html.BeginForm("bossSignView", "Home", FormMethod.Post))
{
    <div class="panel panel-default" ng-controller="bossSignViewController">
        <div class="panel-heading">
            <div class="row">
                <div class="col-md-2">
                    主管簽核動作
                </div>
                <div class="col-md-4">
                    <input type="button" value="核可" onclick="location.href='@Url.Action("agree", "Home", new
                            {
                               applyRequestID = ViewData["applyRequestID"]
                             })'" />                   
                    <input type="submit" value="不准" id="strRejectReason" onclick="if (document.getElementById('rejectReasonText').value == '') { alert('請輸入不准的原因'); return false };" />
                    @Html.TextBox("rejectReasonText", null)
                    @Html.Hidden("applyRequestID", ViewData["applyRequestID"])       
                </div>
            </div>
        </div>
        <div class="panel-body">
            <div sign-view-template></div>   //呼叫directive的方式        
        </div>
    </div>
}
<script src="~/Scripts/signViewTemplate.js"></script>


l   Factory 應用

angular
  angular.module('requestApp', []).factory('myService', function ($http) {
        var listRdusers;
        return {
            getFoo: function () {
                return $http.get("../api/GetUser").then(function (result) { //呼叫Web API
                    listRdusers = result.data;
                    return listRdusers;
                });
            }
        }
    }).controller('getRDUser', function ($scope, myService) { //注入factory
        myService.getFoo().then(function (response) {           
            $scope.taskusers = response; // set value to $scope
            $scope.taskUser = response[0];
        })
    })

html
<div class="row" ng-app="requestApp">
    <div class="col-md-4" ng-controller="getRDUser">
        <select id="taskUser" ng-model="taskUser" ng-options="obj as obj.strRoleUserName for obj in taskusers"></select>
    </div>
</div>

五、     Asp.net mvc5 Session 啟用
因為安全性的問題,asp.net MVCsession 需要設定啟動模式,步驟如下所示,作者也吃了不少苦頭,不過安全起見,能避免還是改用其他方式。

Web.config
<system.webServer>
    <modules>
      <remove name="Session" />
      <add name="Session" type="System.Web.SessionState.SessionStateModule"/>
    </modules>
  </system.webServer>

Global.aspx
  protected void Session_Start()
        {
            Session["intApplyRequestID"] = "";  //初始化session
            Session["uid"] = "";
        }
        protected void Session_End()
        {     
  }

針對Session State 還有模式可運用,請參考相關資料


六、     使用google server發送gmail 信件

系統有自動發送e-mail 通知功能,但公司又沒架SMTP, 此時google的伺服器SMTP 就非常的方便,以下範例僅供參考

private static void commonSend(entityRequest entityRequestObj, string strEndMail, string strRoleUserName)
        {
            MailMessage message = new MailMessage("帳號@gmail.com", strEndMail);
            message.IsBodyHtml = true;
            message.BodyEncoding = System.Text.Encoding.UTF8;
            string applyResult = "";
            string strContext = "";         

            message.Subject = "申請人:" + entityRequestObj.applyUserName + " 提出需求日期 : " + entityRequestObj.requestDate.Value.ToString("yyyy/MM/dd")
            strContext += "申請人: " + entityRequestObj.applyUserName + "<br>";
            strContext += "申請日期: " + entityRequestObj.requestDate.Value.ToString("yyyy/MM/dd") + "<br>";
            strContext += "申請理由: " + entityRequestObj.applyReason + "<br>";
            strContext += "流程: " + entityRequestObj.processName + "<br>";
            strContext += "申請結果: " + applyResult + "<br>";
            if (entityRequestObj.predictCompleteDate != null)
            {
                strContext += "預計完工日: " + entityRequestObj.predictCompleteDate.Value.ToString("yyyy/MM/dd");
            }
            message.Body = strContext;
            var smtpClient = new SmtpClient("smtp.gmail.com", 25)
            {
                EnableSsl = true,
                DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network,
                UseDefaultCredentials = false,
                Credentials = new NetworkCredential("Gmail帳號@gmail.com", "密碼"), //設定發送gmail的帳號
            };
            smtpClient.EnableSsl = true;
            smtpClient.Send(message);
        }


    感謝大家有耐心的看完,也歡迎大家分享相關的經驗談,Angular 的語法不同JQuery,應用上些不習慣,有時混合一些JQuery 語法也許是投機取巧的方式,善用工具的利弊,對大家的專案速度和能力也有所提升

沒有留言:

張貼留言