[Agentflow開發記事]--WFCI 與 自訂表單的相遇

上週跑去新竹華苓上了 Agentflow AF3-4 (System Integration and Customization with Agentflow)的課程,上完了直覺的值回票價,比你在 Agentflow Forum 中大海撈針來的有用許多!(建議AF3-2 & AF3-4都要上過)


要進行整合,可以使用的方式有三種

1. 自訂表單
2. WFCI
3. Web Services
第一種方式較為單純,就是提供一組 script 嵌入原本的 Web App。透過流程設定的自訂文件,在簽核時,將表單 redirect 到您的 Web App,同時將流程所使用的參數透過 Form 的包裹方式,Post到您的Web App,讓您的 Web App 可以使用,算起來是最為便利的,也是最快的方式,但是就受限於表單的資料定義項目以及所提供的 Web 參數,同時只能從WebAgenda去啟動流程,在整合度上相對受到限制。
第二種方式,是一組完整的API讓您的程式可以直接跟Agentflow(PASE Server)溝通,同時可以完全客製出您的Client端(相對於Agentflow Portal),但缺點是只支援Java Program(Java AP & JSP),如果要讓 .Net App使用,則必須透過 .Net Java Plug-IniKVM.Net)將 jar 檔轉換成 dll .Net 使用。
第三種方式,則是Follow標準的Web Service作法,由Agentflow所提供的 Web Services來跟Agentflow溝通(設定方式詳見上一篇),走的是標準的SOAP協定,所以在效能上會大打折扣(當然,這得視您要傳輸的資料量大小有關)。
這邊有一個觀念,必須先釐清楚:以前在開發Notes Workflow AP時,常聽到的說法就是NotesWorkflow是屬於Form Based的流程引擎(Agentflow在流程設計上如果照字面來看,某種程度上也算是,因為一定需要提供一個表單,但是把它當作流程所需使用資訊的Table即可);也就是說,流程必須綁住表單,流程的資料都來自於表單(這種說法,基本上是對的),所以流程無法與表單抽離這樣的說法基本上是一種謬誤!如果我們將流程需使用的表單,當做是流程資料的承載,將使用者實際面對的表單,當做是資料的展現,這樣我就可以將使用者表單與流程抽離,我的使用者表單負責的是要呈現給使用者的資訊,當我需要跑流程的時候,我再透過呼叫流程API的方式,將流程會使用到的資料寫入流程使用的表單,讓流程知道怎麼跑像下一關即可,我的使用者表單根本不需要管流程怎麼跑,這也是當初我拿Lotus Workflow當做流程引擎,用網頁來設計使用者表單的概念(這中間使用到一些AJAX的技巧,請搜尋一下本網誌 Domino主題)。

那到底要怎麼使用才會比較合適呢?如果我是.Net的開發者,是不是會很困難?不像Java 開發那樣擁有較多的Resource
基本上,我是把Web Service先排除掉了,因為設定上比較麻煩,中間多走了一層Protocol,但是我如果完全要透過WFCI重寫,那麼我舊的AP又都沒辦法使用,等於是浪費人力與時間成本,但是使用自訂表單,又只能從WebAgenda去跑流程該怎麼處理呢?
於是答案都在影片中….XD
前面我們提過,WFCI只支援Java/JSP,其實我需要的是由外部的程式啟動流程,然後使用者可以在WebAgenda中去簽核表單,也可以追蹤表單,也就是說由WenAgenda來幫我管理簽核流程。要達到這樣的功能,又不想重寫程式,那我們就需要透過自訂表單的協助,同時由WFCI來啟動流程可是我的程式是 .Net開發的,怎麼去呼叫WFCI啊!
答案在JSP!(當然你也可以用Web Services
我們可以寫一支JSP程式,專門用來讓外部程式啟動流程。(當然這是上課的教材Copy過來改的)
外部的程式只要將要啟動的流程ID,啟動流程的使用者,以及要傳給自訂表單的參數包裹成HTTP Form Data POST initProcess.jsp即可啟動流程。


底下,就為您示範如何在 Web App 中將 CustForm中所定義的資料欄位包裹起來,POST到 initProcess.jsp 透過 WFCI 來啟動 Agentflow 中的流程。

InitProcess.aspx.vb
  1. Imports System
  2. Imports System.Collections.Generic
  3. Imports System.Text
  4. Imports System.Net
  5. Imports System.Collections.Specialized
  6. Imports System.IO
  7. Partial Class Forms_HttpRpc_frmDCInitProcess
  8. Inherits System.Web.UI.Page
  9. Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
  10. Dim httpPost As HttpPostCallWebSite = New HttpPostCallWebSite()
  11. Dim keyValuePair As NameValueCollection = New NameValueCollection()
  12. keyValuePair.Add("processID", "PRO00431303438269342") ' 流程 ID
  13. keyValuePair.Add("userID", Request("wm_id")) ' 啟動流程的人員 ID
  14. keyValuePair.Add("pm_id", Request("pm_id")) ' CustForm 定義的表單欄位
  15. keyValuePair.Add("csr_id", Request("csr_id")) ' CustForm 定義的表單欄位
  16. keyValuePair.Add("wm_id", Request("wm_id")) ' CustForm 定義的表單欄位
  17. keyValuePair.Add("PB_NO", Request("PB_NO")) ' CustForm 定義的表單欄位
  18. Dim url As String = "http://localhost:8080/WebAgenda/initProcess.jsp"
  19. Dim result As String = httpPost.RequestHttpPost(url, keyValuePair)
  20. Response.Write(result)
  21. End Sub
  22. End Class

  23. ' POST data class
  24. Public Class HttpPostCallWebSite
  25. Inherits System.Web.UI.Page
  26. Public Function RequestHttpPost(ByVal webUrl As String, ByVal keyValuePair As NameValueCollection) As String
  27. Dim ret As String
  28. ' Create a request using a URL that can receive a post.
  29. Dim request As WebRequest = WebRequest.Create(webUrl)
  30. ' Set the Method property of the request to POST.
  31. request.Method = "POST"
  32. 'Set Http Post Params
  33. Dim strParams As String = ""
  34. Try
  35. Dim i As Integer
  36. For i = 0 To keyValuePair.Count - 1
  37. strParams += keyValuePair.GetKey(i) + "=" + keyValuePair.Get(i) + "&"
  38. Next i
  39. strParams = IIf(Right(strParams, 1).Equals("&"), Left(strParams, Len(strParams) - 1), strParams)
  40. ' Create POST data and convert it to a byte array.
  41. Dim byteArray As Byte() = Encoding.UTF8.GetBytes(strParams)
  42. ' Set the ContentType property of the WebRequest.
  43. request.ContentType = "application/x-www-form-urlencoded"
  44. ' Set the ContentLength property of the WebRequest.
  45. request.ContentLength = byteArray.Length
  46. ' Get the request stream.
  47. Dim dataStream As Stream = request.GetRequestStream()
  48. ' Write the data to the request stream.
  49. dataStream.Write(byteArray, 0, byteArray.Length)
  50. ' Close the Stream object.
  51. dataStream.Close()
  52. ' Get the response.
  53. Dim response As WebResponse = request.GetResponse()
  54. ' Display the status.
  55. ret = CType(response, HttpWebResponse).StatusDescription
  56. ' Get the stream containing content returned by the server.
  57. dataStream = response.GetResponseStream()
  58. ' Open the stream using a StreamReader for easy access.
  59. Dim reader As New StreamReader(dataStream)
  60. ' Read the content.
  61. Dim responseFromServer As String = reader.ReadToEnd()
  62. ' Clean up the streams.
  63. reader.Close()
  64. dataStream.Close()
  65. response.Close()
  66. Return ret & " " & responseFromServer
  67. Catch ex As Exception
  68. Return "Para:" & strParams & ex.Message
  69. End Try
  70. End Function
  71. End Class
上面這支AP就是我們外部程式,透過 WebRequest 這個Class來呼叫啟動流程的 jsp 程式。接下來,我們就來看 initProcess.jsp 該如何撰寫。

initProcess.jsp
  1. <%@ page language="java" contentType="text/html;charset=UTF-8" %>
  2. <%@ page import="java.io.*,java.util.*,
  3. java.util.HashMap,
  4. java.util.Vector,
  5. si.wfinterface.WFCI,
  6. si.wfcidata.WFCIException,
  7. si.WFCIFactory,
  8. pe.pase.MemberRecord,
  9. pe.pase.Role,
  10. pe.pase.DBProcess,
  11. pe.pase.Task,
  12. pe.pase.PASEartInstance" %>
  13. <%
  14. String PASE_IP = "localhost";
  15. String PASE_PORT = "1099";
  16. WFCI wfci = null;
  17. String processID="";
  18. String userID="";
  19. //wfci = WebSystem.getWFCI();
  20. //if (wfci==null) {
  21. try {
  22. wfci = WFCIFactory.createWFCIRMIImpl();
  23. if (wfci.connectServer(PASE_IP, PASE_PORT) == -1) {
  24. throw new Exception("can't connect Flow Server: host=" + PASE_IP + " Port=" + PASE_PORT);
  25. }
  26. HashMap dataMap = new HashMap(); //傳送資料
  27. // 接收傳進來的 Form data
  28. Enumeration paramNames = request.getParameterNames();
  29. while(paramNames.hasMoreElements()) {
  30. String paramName = (String)paramNames.nextElement();
  31. out.print("" + paramName + "\n");
  32. String paramValue = request.getParameter(paramName);
  33. out.println(" " + paramValue + "\n");
  34. if ("processID".equals(paramName)){
  35. processID=paramValue;
  36. } else if ("userID".equals(paramName)){
  37. userID=paramValue;
  38. } else {
  39. dataMap.put(paramName,paramValue);
  40. }
  41. }
  42. //取得人員相關資料
  43. MemberRecord MR = wfci.getMember(userID);
  44. if (MR != null){
  45. String userName = MR.getName();
  46. //取得人員職務相關資料
  47. String roleID = MR.getMainRoleID();
  48. Role userRole = wfci.getRole(roleID);
  49. String roleName = userRole.getName();
  50. out.println("createProcess from UserName:"+userName+"useRoleName:"+roleName);
  51. //取得流程相關資料
  52. DBProcess DBp = wfci.getDBProcess(processID);
  53. if (DBp != null){
  54. out.println("createPrcoessName :"+DBp.getName());
  55. }
  56. //createProcess return RootTask
  57. String taskID = wfci.createProcess(userID,processID,dataMap,false);
  58. if (taskID != null){
  59. out.println("createProcess success!!");
  60. Task rootTask = wfci.getTask(taskID);
  61. out.println("rootTask Type is :"+rootTask.getTaskType());
  62. String rootProcessID = rootTask.getProcessID();
  63. DBProcess rootProcess = wfci.getDBProcess(rootProcessID);
  64. out.println("rootTask proceeName:"+rootProcess.getName());
  65. }
  66. }else{
  67. out.println("can't get "+userID+" MemberRecoed");
  68. }
  69. }catch(WFCIException e){
  70. out.println("catch wfci throws Exception : "+e);
  71. e.printStackTrace();
  72. }catch(Exception e){
  73. out.println("catch Exception : "+e);
  74. e.printStackTrace();
  75. }finally{
  76. //must disconnect
  77. if (wfci != null)
  78. wfci.disconnectServer();
  79. }
  80. //}
  81. %>

粗體字的部份,就是呼叫 WFCI 啟動流程的重點,Form data 的接收,透過
Enumeration paramNames = request.getParameterNames();
可以將資料一個一個取出來,再組成 HashMap丟到流程裡。
這樣我們就把流程啟動了!(得意)
噫?沒動靜!

對!少了一個步驟,流程必須知道你送過來的資料是什麼,才能夠把資料存到 CustForm 定義的資料欄位裡面!

所以我們在第一個關卡的 PreAction 這個 Script 區塊中,要加上底下這一段程式碼
  1. var hm = Server.getGlobals(MyTask.getRootID());
  2. var Artins = MyTask.getArtInstance();
  3. var pm_id = hm.get("pm_id");
  4. var csr_id = hm.get("csr_id");
  5. var wm_id = hm.get("wm_id");
  6. var PB_NO = hm.get("PB_NO");
  7. Artins.setAppValue("pm_id",pm_id);
  8. Artins.setAppValue("csr_id",csr_id);
  9. Artins.setAppValue("wm_id",wm_id);
  10. Artins.setAppValue("PB_NO",PB_NO);
  11. //Artins.setAppValue("pm_id",hm.get("pm_id"));
  12. java.lang.System.out.println("======== pm_id:"+pm_id);
  13. java.lang.System.out.println("======== csr_id:"+csr_id);
  14. java.lang.System.out.println("======== wm_id:"+wm_id);
  15. java.lang.System.out.println("======== PB_NO:"+PB_NO);
這樣就能夠啟動流程並與舊有的 AP 結合使用了!(請參考 AF3-4 講義中的作法,底下的程式碼,因為顯示的問題所以加上了一些空格讓程式碼可以顯示完整)
  1. < script src="<%=Request("eform.web.url") %>/eform/customform.js" type="text/javascript" >< /script >
  2. < form id="eForm" action="<%=Request("eform.web.url") %>/custForm.do" method="post" >
  3. < input name="eform.method" type="hidden" value="suspend" / >
  4. < input name="eform.taskId" type="hidden" value="<%=Request("eform.taskId")%>" / >
  5. < input name="pb_no" type="hidden" value="<%=Request("pb_no")%>" / >
  6. < /form >



7 意見

請教後來怎選Agentflow不用Lotus主要原因是?

Reply

主要是平台過舊,email Client 不如 Outlook 好用

在上面開發流程與表單,要整合並不容易

不想在花錢投資在這上頭

Reply

James, 請問您熟悉AgentFlow嗎?

Reply

James, 可以請教您一些AgentFlow的問題嗎?

Reply

James, 可以跟您請教一些AgentFlow的問題嗎?

Reply

Hi, 瑋叡

您可以發 mail 到我的信箱

jamesjantw@gmail.com

歡迎一起討論

Reply

您好..

拜讀您的這篇文章,有實際操作一次,我是用.NET寫的自訂表單,放到AGENTFLOW後,沒辦法像一般表單可以跑流程,不知是否還有其他地方應注意呢?

Reply

張貼留言