Appnote-Sample Workflow Agents
Appnote-Sample Workflow Agents
Application Note
Date August 24, 2011
Applies To Kofax Capture 8.0, 9.0, 10.0 Ascent Capture 7.x Summary Revision This application note provides a few sample Workflow Agents for the Capture 7.x, 8.0, 9.0 and 10.0 products. 2.2
Contents
Move Loose Pages Workflow Agent......................................................................................................... 2 Get the Data Type of All Index Fields in a Batch Workflow Agent ............................................................. 3 Data Lookup Workflow Agent................................................................................................................... 6 A Custom Batch History Workflow Agent ................................................................................................. 8 Externally Programmable Required Field Workflow Agent ...................................................................... 10 Programmatically Set a Batch Field ....................................................................................................... 13 Programmatically Set a Batch Priority .................................................................................................... 14 A Batch Totaling Workflow Agent ........................................................................................................... 16 Parsing the Original File Name .............................................................................................................. 17
Overview
This application note provides a few sample Workflow Agents written in the C# language that demonstrate some solutions to some typical scenarios.
namespace MoveLoosePages { [Guid("167BAE04-0127-4280-AA57-B199D255C97A")] [ClassInterface(ClassInterfaceType.None)] [ProgId("MoveLoosePages.LoosePages")] public class LoosePages : _IACWorkflowAgent { public void ProcessWorkflow(ref Kofax.ACWFLib.ACWorkflowData oWorkflowData) { // We only want to do the separation after Scan. if (oWorkflowData.CurrentModule.ID == "scan.exe") { // First get the Root & Batch elements. ACDataElement oRoot = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oBatch = oRoot.FindChildElementByName("Batch"); // Get the Document collection element. ACDataElement oDocuments = oBatch.FindChildElementByName("Documents"); if (oDocuments == null) { oBatch.CreateChildElement("Documents"); oDocuments = oBatch.FindChildElementByName("Documents"); } // Get the Pages element and collection from the Batch. These are Loose Pages. ACDataElement oPages = oBatch.FindChildElementByName("Pages"); ACDataElementCollection oPageCol = oPages.FindChildElementsByName("Page"); // Create a new Document element then iterate through the // Batch Pages collection moving each to the new Document. if (oPageCol.Count > 0) { // Get the Document's Pages element. ACDataElement oDoc = oDocuments.CreateChildElement("Document"); ACDataElement oDocPages = oDoc.FindChildElementByName("Pages"); if (oDocPages == null) { oDoc.CreateChildElement("Pages"); oDocPages = oDoc.FindChildElementByName("Pages"); } foreach(ACDataElement oDocPage in oPageCol) { // When moving Pages into the Document, they are // moved into the Document's Pages element. try { oDocPage.MoveToBack(ref oDocPages); } catch (Exception ex) { oWorkflowData.ErrorText = ex.Message; } } }
}}}}
Listing 1
Page 2
Get the Data Type of All Index Fields in a Batch Workflow Agent
Consider that there is a requirement for obtaining the data type for all the Index Fields in a Batch. One reason could be to determine the data type for insertion into a database at some later point in the workflow. This can be done in a Workflow Agent, but requires using both the AscentCaptureRuntime and AscentCaptureSetup ACDataElements. These are defined in the following files: Ascent Capture 7.x C:\Program Files\AscentSS\CaptureSV\AcBatch.htm C:\Program Files\AscentSS \CaptureSV\AcSetup.htm
Kofax Capture 8.0, 9.0, and 10.0 C:\Documents and Settings\All Users\Application Data\Kofax\CaptureSV\AcBatch.htm C:\Documents and Settings\All Users\Application Data\Kofax\CaptureSV\AcSetup.htm
The flow to obtain Index Field data types follows this general path:
Get the runtime root and current Batch ACDataElements Get the Documents collection from the Batch Get the BatchClass element associated with the current Batch
Output results
Get the Document Classes for the Documents in the current Batch
The source code for the project is located on the next two pages.
Page 3
namespace WFAGetFieldInfo { [Guid("ED1B171D-C72B-4152-972C-2B1F3C4E7F49")] [ClassInterface(ClassInterfaceType.None)] [ProgId("WFAGetFieldInfo.FieldInfo")] public class FieldInfo : _IACWorkflowAgent { public void ProcessWorkflow(ref Kofax.ACWFLib.ACWorkflowData oWorkflowData) { if (oWorkflowData.CurrentModule.ID.ToLower() == "scan.exe") { // Get the root & Batch elements. ACDataElement oRoot = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oBatch = oRoot.FindChildElementByName("Batch"); // Get the Documents collection. ACDataElement oDoc = oBatch.FindChildElementByName("Documents"); ACDataElementCollection oDocCol = oDoc.FindChildElementsByName("Document"); // Get the SetupACDataElement root. ACDataElement oSetupRoot = oWorkflowData.ExtractSetupACDataElement(0); // Obtain the Batch Class definition of the current Batch. ACDataElement oCurrBatchClass = GetCurrentBatchClass(oSetupRoot, oBatch); // Instantiate a generic list to contain the data types for the // Document classes in this Batch. This list is of type DataType // which is defined in a class in this project. List<DataType> lstInfo = new List<DataType>(); // Iterate through the Document Classes in the Batch Class. // Iterate through the Index Fields in the Document Classes. // Add the Field info to the list while ensuring unique objects. // Iterate through the Documents in this Batch. For each Document, // get it's Document Class and iterate through it's Index Fields // to get the field types. foreach (ACDataElement doc in oDocCol) { ACDataElement oDocClass = GetDocumentClass(doc, oCurrBatchClass, oSetupRoot); // Iterate through the Index Field collection and populate // the generic list. ACDataElement oIndexDefs = oDocClass.FindChildElementByName("IndexFieldDefinitions"); ACDataElementCollection oIndexDefsCol = oIndexDefs.FindChildElementsByName("IndexFieldDefinition"); foreach (ACDataElement oIndexDef in oIndexDefsCol) { DataType dt = new DataType(); dt.DocumentClass = oDocClass["Name"].ToString(); dt.IndexFieldName = oIndexDef["Name"].ToString(); dt.IndexFieldType = GetIndexType(oIndexDef["FieldTypeName"].ToString(), oSetupRoot); lstInfo.Add(dt); } } // Now that we have a generic List populated with the information // we need, we can now do with it what we want. In this case, we // are simply going to write the List items to a file. using (StreamWriter sw = File.AppendText(@"c:\WFATestFile.txt")) { foreach (DataType dt in lstInfo) { sw.WriteLine("Document Class: " + dt.DocumentClass); sw.WriteLine("Index Field : " + dt.IndexFieldName); sw.WriteLine("Field Type : " + dt.IndexFieldType); sw.WriteLine(" "); } } } } private ACDataElement GetCurrentBatchClass(ACDataElement SetupRoot, ACDataElement CurrBatch) { // Get the Batch Class definition element for the current Batch. ACDataElement oBatchClass = SetupRoot.FindChildElementByName("BatchClasses"); ACDataElementCollection oBatchClassCol = oBatchClass.FindChildElementsByName("BatchClass");
Listing 2
Page 4
}}
Listing 2 (continued)
Page 5
Page 6
namespace DataLookup { [Guid("9B3281D9-6C5B-4dee-9209-E55A23984CBE")] [ClassInterface(ClassInterfaceType.None)] [ProgId("DataLookup.SequenceNumber")] public class SequenceNumber : _IACWorkflowAgent { public void ProcessWorkflow(ref Kofax.ACWFLib.ACWorkflowData oWorkflowData) { if (oWorkflowData.CurrentModule.ID.ToLower() == "scan.exe") { // Set the Index Field Value ACDataElement root = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oBatch = root.FindChildElementByName("Batch"); // Determine if this is the correct Batch Class if (oBatch["BatchClassName"].ToString() == "Test Numeric") { using (SqlConnection oConn = new SqlConnection( @"Data Source=mrobertson-wks\sqlexpress;Initial Catalog=TestData;Integrated Security=True")) { try { // Create a database connection and open the database oConn.Open(); // Start a transaction process SqlTransaction oTran = oConn.BeginTransaction(); ACDataElement oDocs = oBatch.FindChildElementByName("Documents"); ACDataElementCollection oDocCol = oDocs.FindChildElementsByName("Document"); foreach(ACDataElement oDoc in oDocCol) { // Connect to an SQL Server database, get the current sequence value, // assign the value to an Index Field, increment the value then // update the value in the SQL Server database. int SeqNum = 0; // Retrieve the value out of the database and set the Index Field to the value SqlCommand oCmdSel = new SqlCommand("SELECT TOP 1 SequenceNumber FROM SequenceNumber", oConn); oCmdSel.Transaction = oTran; SeqNum = int.Parse(oCmdSel.ExecuteScalar().ToString()); // Set the sequence value into the Document Index Field ACDataElement oIndexes = oDoc.FindChildElementByName("IndexFields"); ACDataElementCollection oIndexesCol = oIndexes.FindChildElementsByName("IndexField"); foreach (ACDataElement oIndex in oIndexesCol) { if (oIndex["Name"] == "Number Field") { oIndex["Value"] = SeqNum.ToString(); break; } } // Update the Sequence in the database with the incremented value. SeqNum++; SqlCommand oCmdUpdate = new SqlCommand("UPDATE SequenceNumber SET SequenceNumber=" + SeqNum.ToString(), oConn); oCmdUpdate.Transaction = oTran; oCmdUpdate.ExecuteNonQuery(); } // Commit the transaction oTran.Commit(); } catch (Exception ex) { oWorkflowData.ErrorText = ex.Message; } } // using SqlConnection } // if BatchClassName } // if ModuleID = scan.exe } } }
Listing 3
Page 7
And after running a Batch, this was the output in the table:
As the entries show, the Batch workflow contains the Scan, Recognition Server and Validation modules. NOTE: Workflow Agents do not fire after Release.
Page 8
namespace BatchHistory { [Guid("E1D00C79-C8F6-4a70-849F-8E7FF7DFB7E8")] [ClassInterface(ClassInterfaceType.None)] [ProgId("BatchHistory.History")] public class History : _IACWorkflowAgent { public void ProcessWorkflow(ref Kofax.ACWFLib.ACWorkflowData oWorkflowData) { if (oWorkflowData.CurrentModule.ID == "scan.exe" || oWorkflowData.CurrentModule.ID == "fp.exe" || oWorkflowData.CurrentModule.ID == "index.exe") { // First drill down and get the Batch ACDataElement and the // BatchHistories collection. ACDataElement root = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oBatch = root.FindChildElementByName("Batch"); ACDataElement oHistory = oBatch.FindChildElementByName("BatchHistoryEntries"); ACDataElementCollection oHistoryCol = oHistory.FindChildElementsByName("BatchHistoryEntry"); // Get the last entry in the collection. This should be for the current ModuleID. // However we will check to make sure. ACDataElement oHist = oHistoryCol[oHistoryCol.Count]; if (oHist["ModuleID"].ToString() == oWorkflowData.CurrentModule.ID.ToString()) { // We found the element. Insert a record into the database. using (SqlConnection oConn = new SqlConnection( @"Data Source=mrobertson-wks\sqlexpress;Initial Catalog=TestData;Integrated Security=True")) { try { // Create a database connection and open the database oConn.Open(); // Start a transaction process SqlTransaction oTran = oConn.BeginTransaction(); string SQLInsert = "INSERT INTO BatchHistory (BatchID, ModuleID, UserID, TimeStart, TimeEnd)" + "VALUES ('" + oBatch["ExternalBatchID"].ToString() + "', '" + oWorkflowData.CurrentModule.ID.ToString() + "', '" + oHist["UserID"].ToString() + "', '" + oHist["StartDateTime"].ToString() + "', '" + DateTime.Now.ToString() + "')"; SqlCommand oCmdInsert = new SqlCommand(SQLInsert, oConn); oCmdInsert.Transaction = oTran; oCmdInsert.ExecuteNonQuery(); oTran.Commit(); } catch (Exception ex) { // Put exception handling here. } } }
Listing 4
Page 9
For this Workflow Agent, we will be using a LINQ query to obtain the Index Field names from the XML file. Here is the LINQ query:
IEnumerable<XElement> ireqFields = (from f in xFile.Element("IndexFields").Elements("IndexField") select f);
The complete source code for the Workflow Agent is in Listing 5, beginning on the next page. NOTE: Visual Studio 2008 (.NET 3.5) is required to use the LINQ libraries.
Page 10
namespace RequiredFieldsWFA { [Guid("DA01B20B-2610-4f7d-8B3D-BB6FDD82E31F")] [ClassInterface(ClassInterfaceType.None)] [ProgId("RequiredFieldsWFA.RequiredFields")] public class RequiredFields : _IACWorkflowAgent { public void ProcessWorkflow(ref Kofax.ACWFLib.ACWorkflowData oWorkflowData) { ACDataElement oRoot = null; ACDataElement oBatch = null; ACDataElement oDocs = null; ACDataElementCollection oDocsCol = null; ACDataElement oIndexes = null; ACDataElementCollection oIndexesCol = null; try { // This code only executes after Recognition. if (oWorkflowData.CurrentModule.ID.ToLower() == "fp.exe") { string sXMLPath = Path.Combine(Application.StartupPath, "RequiredFields.xml"); using (Microsoft.Win32.RegistryKey rk = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Kofax Image Products\Ascent Capture\3.0", false)) { sXMLPath = Path.Combine(rk.GetValue("ServerPath").ToString(), "RequiredFields.xml"); } List<string> ReqFieldsList = new List<string>(); // Make sure that the XML file exists. if (!File.Exists(sXMLPath)) { throw new ApplicationException("The RequiredFields.xml file was not present."); } // Load up the XML file and populate the generic List // with the required Index Field names. XDocument xFile = XDocument.Load(sXMLPath); XElement reqFields = xFile.Element("IndexFields"); // This LINQ query gets all the Field names from the // IndexFields top element. IEnumerable<XElement> ireqFields = (from f in xFile.Element("IndexFields").Elements("IndexField") select f); foreach (XElement x in ireqFields) { ReqFieldsList.Add(x.Attribute("name").Value.ToString()); } // Get the ACDataElements oRoot = oWorkflowData.ExtractRuntimeACDataElement(0); oBatch = oRoot.FindChildElementByName("Batch"); oDocs = oBatch.FindChildElementByName("Documents"); oDocsCol = oDocs.FindChildElementsByName("Document"); bool bNotPopulated = false; // Iterate through the Documents foreach (ACDataElement oDoc in oDocsCol) { // Iterate the Index Fields in the Document // If an unpopulated required field is detected, // set the flag and exit the loops. oIndexes = oDoc.FindChildElementByName("IndexFields"); oIndexesCol = oIndexes.FindChildElementsByName("IndexField"); foreach (ACDataElement oIndex in oIndexesCol) { if (ReqFieldsList.Exists(ap => ap == oIndex["Name"].ToString())) { bNotPopulated = oIndex["Value"].ToString().Trim() == "" ? true : false; } Marshal.ReleaseComObject(oIndex); if (bNotPopulated) break;
Listing 5
Page 11
Listing 5 (continued)
Page 12
Create a Batch Class containing a Batch Field. In this case we named the Field TestValue. Make the Batch Field hidden so that the user cannot set it at Batch creation. In all Document Classes in this Batch Class, map the Default value to {$TestValue}, which is the Batch Field. Create a Workflow Agent to set the Batch Field value (see Listing 6).
System; System.Collections.Generic; System.Text; Kofax.ACWFLib; Kofax.DBLite; Kofax.DBLiteOpt; System.Runtime.InteropServices;
namespace WFACopyBatchField { [Guid("FEB73FE4-FBAE-46db-B25E-E4597727F0C6")] [ClassInterface(ClassInterfaceType.None)] [ProgId("WFACopyBatchField.BatchField")] public class BatchField : _IACWorkflowAgent { public void ProcessWorkflow(ref Kofax.ACWFLib.ACWorkflowData oWorkflowData) { // This WFA processes after the Recognition Module. if (oWorkflowData.CurrentModule.ID.ToLower() == "fp.exe") { // Retrieve the Batch element. ACDataElement root = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oBatch = root.FindChildElementByName("Batch"); // // // if { Retrieve the Batch Field to copy. The Field name is hard coded because this WFA will only be used with this Batch Class. However, we still check the Batch Class name. (oBatch["BatchClassName"] == "TEST") // Retrieve the BatchField element with the Name attribute = TestValue. ACDataElement oFields = oBatch.FindChildElementByName("BatchFields"); ACDataElement oField = oFields.FindChildElementByAttribute("BatchField", "Name", "TestValue"); oField["Value"] = DateTime.Now.ToString(); } } } } }
Listing 6 Note the use of the FindChildElementByName( ) method. This method takes three parameters. In this case, we are retrieving the BatchField element with the Name attribute equaling TestValue. The Value attribute of this element is then set to the current system Date and Time. This value will be propagated to all Documents that have a Field which was properly mapped to this Batch-level Field.
Page 13
No match found
Yes
Page 14
namespace SetPriority { [Guid("5B9A4F33-4FAD-4288-9FEC-7897777F2FB7")] [ClassInterface(ClassInterfaceType.None)] [ProgId("SetPriority.SetPriority")] public class SetPriority : _IACWorkflowAgent { public void ProcessWorkflow(ref ACWorkflowData oWorkflowData) { // This executes after the Recognition Module. if (oWorkflowData.CurrentModule.ID.ToLower() == "fp.exe") { //global::System.Windows.Forms.MessageBox.Show("Test3"); ACDataElement root = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oBatch = root.FindChildElementByName("Batch"); if (IsPriorityDocument(oWorkflowData)) { oBatch["Priority"] = "1"; } } } private bool IsPriorityDocument(ACWorkflowData oWorkflowData) { // We will need both the Runtime and the SetupRuntime ACDataElements. ACDataElement oRoot = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oSetup = oWorkflowData.ExtractSetupACDataElement(0); ACDataElement oBatch = oRoot.FindChildElementByName("Batch"); ACDataElement oDocument = oBatch.FindChildElementByName("Documents"); ACDataElementCollection oDocColl = oDocument.FindChildElementsByName("Document"); // for each Document in this current Batch... foreach (ACDataElement oDoc in oDocColl) { // Get the setup info of the Document's Class settings... ACDataElement oDocClass = oSetup.FindChildElementByName("DocumentClasses"); ACDataElementCollection oDocClassColl = oDocClass.FindChildElementsByName("DocumentClass"); // For each Document Class in the Setup root... foreach (ACDataElement oSetupDoc in oDocClassColl) { ACDataElement oFormType = oSetupDoc.FindChildElementByName("FormTypes"); ACDataElementCollection oFormTypeColl = oFormType.FindChildElementsByName("FormType"); // Iterate through the FormTypes collection. foreach (ACDataElement oForm in oFormTypeColl) { if (oForm["Name"] == oDoc["FormTypeName"]) { if (oSetupDoc["Name"] == "MyClassName") { // Document Class Name found. return true; } break; } } } } return false; } } }
Listing 7 Here we are setting the Batch Priority when a Document Class named MyClassName exists in the Batch. Instead of hard coding the name, an external value might be used from a database, text file or XML file. Another method for trapping a Document Class Name is to implement a hidden field in the Document Class then setting its default value to {Document Class Name}. The Workflow Agent could then simply look at the Index value and set the priority attribute accordingly.
Page 15
namespace WFADocTotal { [Guid("35E8AAE9-45D0-43d7-B75C-A89EAAC26846")] [ClassInterface(ClassInterfaceType.None)] [ProgId("WFADocTotal.DocTotal")] public class DocTotal : _IACWorkflowAgent { public void ProcessWorkflow(ref ACWorkflowData oWorkflowData) { // This Workflow Agent processes after the Validation Module. if (oWorkflowData.CurrentModule.ID == "index.exe") { Decimal dTotal = 0; ACDataElement root = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oBatch = root.FindChildElementByName("Batch"); ACDataElement oDocument = oBatch.FindChildElementByName("Documents"); ACDataElementCollection oDocCol = oDocument.FindChildElementsByName("Document"); foreach (ACDataElement oDoc in oDocCol) { // We are doing a blind Try..Catch here because there // could be a Document that may not have an Index Field // with a Name that equals "Sum". try { ACDataElement oIndexes = oDoc.FindChildElementByName("IndexFields"); ACDataElement oSumField = oIndexes.FindChildElementByAttribute("IndexField", "Name", "Sum"); dTotal += Decimal.Parse(oSumField["Value"].ToString()); } catch {} } GC.KeepAlive(oDocCol); ACDataElement oBFields = oBatch.FindChildElementByName("BatchFields"); ACDataElement oBField = oBFields.FindChildElementByAttribute("BatchField", "Name", "Total"); oBField["Value"] = dTotal.ToString(); } } } }
Listing 8 Note the use of the FindChildElementByAttribute on the IndexFields element. This is much simpler than iterating through all the Index Fields looking for a match on the Name attribute.
Page 16
namespace WFAGetOriginalFilename { [Guid("AF07DF16-36F7-44c3-BF40-6BC293EA1D6B")] [ClassInterface(ClassInterfaceType.None)] [ProgId("GetOriginalFilename.GetOriginalFilename")] public class GetOriginalFilename : _IACWorkflowAgent { public void ProcessWorkflow(ref Kofax.ACWFLib.ACWorkflowData oWorkflowData) { if (oWorkflowData.CurrentModule.ID.ToLower() == "scan.exe") { ACDataElement root = oWorkflowData.ExtractRuntimeACDataElement(0); ACDataElement oBatch = root.FindChildElementByName("Batch"); ACDataElement oDocuments = oBatch.FindChildElementByName("Documents"); ACDataElementCollection oDocCol = oDocuments.FindChildElementsByName("Document"); foreach (ACDataElement oDoc in oDocCol) { ACDataElement oPages = oDoc.FindChildElementByName("Pages"); ACDataElementCollection oPagesCol = oPages.FindChildElementsByName("Page"); string[] BaseStr = oPagesCol[1]["OriginalFileName"].ToString().Split('_'); ACDataElement oIndexes = oDoc.FindChildElementByName("IndexFields"); ACDataElement oPart1 = oIndexes.FindChildElementByAttribute("IndexField", "Name", "Part1"); oPart1["Value"] = BaseStr[0]; ACDataElement oPart2 = oIndexes.FindChildElementByAttribute("IndexField", "Name", "Part2"); oPart2["Value"] = BaseStr[1]; ACDataElement oPart3 = oIndexes.FindChildElementByAttribute("IndexField", "Name", "Part3"); oPart3["Value"] = BaseStr[2]; } GC.KeepAlive(oDocCol); } } } }
Listing 9 For our example, the filenames will be three strings concatenated together, separated with underscore characters, (e.g., NAME_ABC_12345.tif). The Workflow Agent iterates through all Documents in the Batch. For each Document, the first Page in the Pages collection is acquired and its OriginalFilename attribute is read and parsed using the Split method. The string values are then written into the three Index Fields, Part1, Part2 and Part3 respectively.
Page 17