As you know, you can create the user interfaces for your human tasks using ADF. JDeveloper allows you to auto-generate a human task user interface (form) and it also includes a wizard that gives you a bit more control over what it produced. Now ADF is a fine framework, but some people already have a pretty heavy investment in some other framework and a lot of these people would really like to be able to use their framework of choice to build their human task user interfaces. And the good news it, they can! And its not that hard to do, once you know how 🙂
In this article, we will look at how you build a human task user interface using C# and ASP.NET. This human task UI will show up right there in BPM workspace, just like the ADF ones do. Here’s what it will look like when it is done:
Those red arrows wont be there! They are just to show you where it is. That part there where you normally see the ADF task form, that is a .Net application.
Now, as I said, you can use any framework to do this – as long as it is capable of calling Java APIs or web services and reading some data from the HTTP Request object’s Query String.
In this post we will build a UI that is specific to the task in question – like the ADF ones that you generate are. But it would not be a long way from here to building a generator so that you could auto-generate .Net user interfaces just like you can for ADF.
What you need
To follow along this post you are going to need a couple of things:
- JDeveloper with the SOA and BPM extensions installed, at least version 11.1.1.5 plus the Feature Pack patch,
- A BPM server, the same version as JDeveloper,
- A copy of Visual Studio with the C# and web applications options installed. I used Visual Studio 2010 Professional, but the free Visual Studio Express editions will also work if you don’t own a copy of Visual Studio. Just make sure you get one with web applications and C# included, and
- either a lot of time and patience to type boring bolierplate code, or a copy of AutoMapper from here. I recommend you take the AutoMapper option…
If you want to get a feel for calling the BPM/HWF APIs/web services from .Net, you might want to review this post.
Creating the composite
First thing we are going to want is a composite to play with. We can just make a really simple one with just a human task in it. That will be enough to do what we want to do here. In fact, something as simple as this will do admirably:
To create this, open up JDeveloper and create a new BPM Application by selecting New from the Application menu. In the wizard choose BPM Application in the Application Template section and give your application a name. I called mine DotNetTest. Then click on Next. Give your project a name, I used the same name. Then click on Next. Choose Composite with BPMN Process and click on Finish.
In the next dialog, give your process a name, I called mine DotNetTest too. Then take the defaults and continue.
Drag a User Task into your process from the component palette and drop it on the line between the Start and End nodes, as shown in the diagram above. Notice that the line turns blue when you hover above it in the right spot. If you don’t see the component palette, you can open it from the View menu.
Apologies that my names of things in this sample are not super imaginative…
Now we need to create some data definitions. Open the BPM Project Navigator. If you don’t see it, you can open it from the View menu. Expand out your project and the Business Catalog. Right click on the Business Catalog and choose New then Module from the popup menu. Call the module Data.
Then right click on your new module and select New then Business Object from the popup menu. Name your new business object. I called mine BusinessObject1 (told you there were not very imaginative). Then click on the green plus icon in the Attributes section to add two new attributes. Make them both of type String. I called mine attribute1 and attribute2. Go ahead and save your work (select Save All from the File menu.)
Now return to your process. Click on the background of the process to make sure the structure pane shows the structure of the process. It should look a bit like this:
Now right click on the Process Data Objects in the structure pane and select New from the popup menu. Give it a name, I chose dataObject1, and choose <Component> for the Type. Then click on the little magnifying glass icon and choose the business object you just defined.
Go ahead and create a second variable (process data object) of type String called dataObject2.
Now let’s set up our data in this process. Right click on the Start node and open its Properties from the popup menu. Go to the Implementation tab and add an argument by clicking on the little green plus icon in the Arguments Definition section. I called mine argument1. Set the type to your business object (Data.BusinessObject1 if you used the same names as me.)
Now click on the Data Associations link. Map your argument1 into your dataObject1 as shown in the image below. Then right click on dataObject2 and choose Expression from the popup menu. Enter “hello” (with the quotes) as the expression. Your data associations should now look a little like this:
That takes care of our inputs. When you start the process you will type in the two strings that get put into dataObject1 and dataObject2 will get set to “hello.”
Now, let’s set up the human task. To make it interesting we are going to allow editing of some data but not of others.
Open the human task properties and go the the Implementation tab. Click on the green plus icon to create a new human task. Give it a name, I called mine Humantask1. Click on the little magnifying glass icon next to Outcomes and set the outcomes to just one option – OK. Then click on the plus icon next to Parameters to open the Browse Data Objects window that you see on the right hand side of the image below. Drag your two data objects into the Parameters area as indicated by the red arrow. Tick the box to make only dataObject2 editable. Then click on OK to complete the human task definition. We will take the defaults for everything else.
Now click on the Data Associations link. Set up the input and output mappings as shown:
Right, now we are ready to go ahead and deploy our composite to our server instance. You can do this back in the Application Navigator using the Deploy option in the popup menu on the project.
Follow through the wizard, I assume that you know how to do this by now if you are a regular reader. If not, you can go ahead and take the defaults on this one.
When the deployment is finished, go to Enterprise Manager (at http://yourserver:7001/em) and login as an administrative user (like weblogic) and then navigate into the SOA folder and you should see your shiny new composite there. Something like this:
Click on it to open the composite page, then click on the Test button and go ahead and launch a couple of instances. We will use them later.
While we are here, let’s tell the runtime that we are planning to use our own user interface for this task. We have not created it yet, but we have a pretty good idea what the details will be. Go back to the composites main page. Down the bottom you should see a list of the components in the composite, including your Humantask1. Click on that to bring up the settings for that component. Then open the Administration tab in these settings.
Click on the green plus icon next to Add URI. Provide the values you see in the image above. We are just going to use the built in test environment in Visual Studio in this post. No need to install IIS for this. Of course in real life you would install it and deploy your web applications to IIS. Make sure you click on the Apply button when you are done to save your changes.
The .Net Application
Now, let’s get to work on the fun part!
Open Visual Studio and start a new project by choosing New then Project from the File menu. In the New Project dialog box, open the Visual C# folder and select the Web category. Make sure you have .Net Framework 4 selected (you will have installed this with Visual Studio most likely – if not, stop and go install it now) and choose the ASP.NET Web Application template. Give your project a name, I called mine WebApplication2. Click on OK to create your application.
We need to tell our project about AutoMapper. First of all, go extract the AutoMapper.zip into your project directory, e.g. c:\users\mark\documents\visual studio 2010\Projects\WebApplication2\WebApplication2. This will give you three files – AutoMapper.dll, AutoMapper.pdb and AutoMapper.xml. For a discussion of what AutoMapper is and why we want it – see this post.
In the Solution Explorer, right click on the References folder and choose Add Reference… from the popup menu.
In the Add Reference window, go to the Browse tab. You should see the AutoMapper.dll that you just unzipped right there in your project directory. Select it and click on OK to add a reference to your project.
Now, we need to add references to the services that we will be using. We need to get the WSDL addresses for the TaskService and the TaskQueryService. You can work these out using the following examples:
http://yourserver:8001/integration/services/TaskQueryService/TaskQueryService?wsdl http://yourserver:8001/integration/services/TaskService/TaskServicePort?wsdl
To add the references, right click on the Service References folder and select Add Service Reference… from the popup menu.
Add the two web services (one at a time). Enter the WSDL URL in the Address field, then click on the Go button. You will see a description of the services avilable as shown in the image below. Enter a name for the service in the Namespace field and then click on the OK button. I called mine TaskService and TaskQueryService.
Now, because these two services use WS-Security, we need to tell .Net to use WS-Security. This is done by editing the web.config file. You should see it right there in the Solution Explorer, go ahead and open it and scroll down to the bottom. Here is the part we are interested in:
<bindings> <basicHttpBinding> <binding name="TaskQueryServiceSOAPBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true"> <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" /> <security mode="None"> <transport clientCredentialType="None" proxyCredentialType="None" realm="" /> <message clientCredentialType="UserName" algorithmSuite="Default" /> </security> </binding> <binding name="TaskServiceSOAPBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true"> <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" /> <security mode="None"> <transport clientCredentialType="None" proxyCredentialType="None" realm="" /> <message clientCredentialType="UserName" algorithmSuite="Default" /> </security> </binding> </basicHttpBinding> </bindings> <client> <endpoint address="http://ps5.mark.oracle.com:8001/integration/services/TaskQueryService/TaskQueryService2/*" binding="basicHttpBinding" bindingConfiguration="TaskQueryServiceSOAPBinding" contract="TaskQueryService.TaskQueryService" name="TaskQueryServicePortSAML" /> <endpoint address="http://ps5.mark.oracle.com:8001/integration/services/TaskQueryService/TaskQueryService" binding="basicHttpBinding" bindingConfiguration="TaskQueryServiceSOAPBinding" contract="TaskQueryService.TaskQueryService" name="TaskQueryServicePort" /> <endpoint address="http://ps5.mark.oracle.com:8001/integration/services/TaskService/TaskServicePort" binding="basicHttpBinding" bindingConfiguration="TaskServiceSOAPBinding" contract="TaskService.TaskService" name="TaskServicePort" /> <endpoint address="http://ps5.mark.oracle.com:8001/integration/services/TaskService/TaskServicePortSAML/*" binding="basicHttpBinding" bindingConfiguration="TaskServiceSOAPBinding" contract="TaskService.TaskService" name="TaskServicePortSAML" /> </client>
For each of the two bindings you will need to update the security section as shown above to use UserName credentials and the Default algorithm for message security. Not transport, message – make sure you get the right one!
Also, which we are here, you might want to note a couple of things in the client section. First, this is where you will go and change the endpoint addresses if you want to use a different server later on. Second, notice that there are two endpoints for each service. One is SAML and one is not. We are going to use the ones that are not SAML in this example. You can make a note of the value in the name attribute for each attribute. We will need those later.
Ok, that takes care of our services. Now let’s do the user interface. We will start by customising the template (Site.Master) first. You don’t strictly need to do this, but it is best to get rid of some of that extra stuff that might cuase confusion. Here is the template I used:
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="WebApplication2.SiteMaster" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head runat="server"> <title></title> <link href="~/Styles/Site.css" rel="stylesheet" type="text/css" /> <asp:ContentPlaceHolder ID="HeadContent" runat="server"> </asp:ContentPlaceHolder> </head> <body> <form runat="server"> <div class="page"> <div class="header"> <div class="title"> <h1> Sample C#/ASP.NET Task form </h1> </div> <div class="loginDisplay"> [ <a href="#" id="HeadLoginView_HeadLoginStatus">Sample</a> ] </div> </div> <div class="main"> <asp:ContentPlaceHolder ID="MainContent" runat="server"/> </div> <div class="clear"> </div> </div> <div class="footer"> </div> </form> </body> </html>
Note that we are running this at the server, not the client, for those who know enough about .Net to care about the difference 🙂
Great, now le’s set up our main page. Open up the Default.aspx page. Here is how we want it to look:
If you want to, you can go and drag and drop everything into place and edit the properties. But in the interest of making this easier for you, and to make sure the names of the UI components match the sample code below, it would be better to copy the code below into the source view.
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication2.QueryStringRecipient" %> <asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent"> </asp:Content> <asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent"> <h2> Task Details </h2> <table> <tr><td>Task Title:</td><td><asp:TextBox ID="TextBox1" runat="server" Enabled="False"></asp:TextBox></td></tr> <tr><td>Task State:</td><td><asp:TextBox ID="TextBox2" runat="server" Enabled="False"></asp:TextBox></td></tr> <tr><td>Task Number:</td><td><asp:TextBox ID="TextBox3" runat="server" Enabled="False"></asp:TextBox></td></tr> </table> <h2> Payload </h2> <table> <tr><td>Updatable Payload Data:</td><td> <asp:TextBox ID="TextBox4" runat="server" AutoPostBack="True"></asp:TextBox></td></tr> <tr><td>Read Only Payload Data:d Only Payload Data:</td><td><asp:TextBox ID="TextBox5" runat="server" Enabled="False"></asp:TextBox></td></tr> </table> <h2> Actions </h2> <asp:Button ID="Button1" runat="server" Text="OK" onclick="Button1_Click" /> </asp:Content>
Great, that’s the UI taken care of. Now let’s put in the code behind it. This is where the really interesting stuff happens 🙂
Go ahead and open up your Default.aspx.cs file and put this code into it:
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; namespace WebApplication2 { public partial class QueryStringRecipient : System.Web.UI.Page { protected System.Web.UI.WebControls.Label lblInfo; private string taskId; private string ctxId; private TaskQueryService.workflowContextType ctx; private TaskQueryService.TaskQueryServiceClient tqs; private TaskQueryService.task task; private void Page_Load(object sender, System.EventArgs e) { // setup the automapper setupAutoMapper(); // BPM will pass us the taskID and the ctx token // need to read these out of the http request's query string taskId = Request.QueryString["bpmWorklistTaskId"]; ctxId = Request.QueryString["bpmWorklistContext"]; //System.Diagnostics.Debug.WriteLine("Task ID:\n" + taskId); //System.Diagnostics.Debug.WriteLine("Context:\n" + ctxId); // if this is running outside of the worklist, just exit if (taskId == null) { // looks like we dont have a task return; } // set up the BPM context ctx = new TaskQueryService.workflowContextType(); ctx.token = ctxId; // get the TQS tqs = new TaskQueryService.TaskQueryServiceClient("TaskQueryServicePort"); // set up the request to get the task TaskQueryService.taskDetailsByIdRequestType getTaskRequest = new TaskQueryService.taskDetailsByIdRequestType(); getTaskRequest.workflowContext = ctx; getTaskRequest.taskId = taskId; // get the task task = tqs.getTaskDetailsById(getTaskRequest); //System.Diagnostics.Debug.WriteLine("task title:\n" + task.title); // populate the UI with task details TextBox1.Text = task.title; TextBox2.Text = task.systemAttributes.state.ToString(); TextBox3.Text = task.systemAttributes.taskNumber; if (!Page.IsPostBack) { // populate the UI with current payload data System.Xml.XmlNode[] payload = (System.Xml.XmlNode[])task.payload; TextBox4.Text = payload.ElementAt(0).ChildNodes.Item(1).InnerText; TextBox5.Text = payload.ElementAt(1).ChildNodes.Item(0).InnerText; } } #region Web Form Designer generated code override protected void OnInit(EventArgs e) { // // CODEGEN: This call is required by the ASP.NET Web Form Designer. // InitializeComponent(); base.OnInit(e); } /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.Load += new System.EventHandler(this.Page_Load); } #endregion protected void Button1_Click(object sender, EventArgs e) { System.Diagnostics.Debug.WriteLine("PRESSED OK"); // update the task payload from the UI System.Xml.XmlNode[] payload = (System.Xml.XmlNode[])task.payload; payload.ElementAt(0).ChildNodes.Item(1).InnerText = TextBox4.Text; task.payload = payload; // get the TS TaskService.TaskServiceClient ts = new TaskService.TaskServiceClient("TaskServicePort"); // update task TaskService.taskServiceContextTaskBaseType updateTaskRequest = new TaskService.taskServiceContextTaskBaseType(); updateTaskRequest.workflowContext = AutoMapper.Mapper.Map<TaskQueryService.workflowContextType, TaskService.workflowContextType>(ctx); updateTaskRequest.task = AutoMapper.Mapper.Map<TaskQueryService.task, TaskService.task>(task); TaskService.task updatedTask = ts.updateTask(updateTaskRequest); // complete task TaskService.updateTaskOutcomeType updateTaskOutcomeRequest = new TaskService.updateTaskOutcomeType(); updateTaskOutcomeRequest.workflowContext = AutoMapper.Mapper.Map<TaskQueryService.workflowContextType, TaskService.workflowContextType>(ctx); updateTaskOutcomeRequest.outcome = "OK"; updateTaskOutcomeRequest.Item = updatedTask; ts.updateTaskOutcome(updateTaskOutcomeRequest); // redirect to empty page Response.Redirect("/Empty.htm"); } private void setupAutoMapper() { // set up the automapper AutoMapper.Mapper.CreateMap<TaskQueryService.workflowContextType, TaskService.workflowContextType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.credentialType, TaskService.credentialType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.task, TaskService.task>(); AutoMapper.Mapper.CreateMap<TaskQueryService.attachmentType, TaskService.attachmentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.callbackType, TaskService.callbackType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.customAttributesType, TaskService.customAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.documentType, TaskService.documentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.EvidenceType, TaskService.EvidenceType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.processType, TaskService.processType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.commentType, TaskService.commentType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.identityType, TaskService.identityType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.ucmMetadataItemType, TaskService.ucmMetadataItemType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.systemAttributesType, TaskService.systemAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.actionType, TaskService.actionType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.displayInfoType, TaskService.displayInfoType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.shortHistoryTaskType, TaskService.shortHistoryTaskType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.assignmentContextType, TaskService.assignmentContextType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.assignmentContextTypeValueType, TaskService.assignmentContextTypeValueType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.collectionTargetType, TaskService.collectionTargetType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.collectionTargetActionType, TaskService.collectionTargetActionType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.preActionUserStepType, TaskService.preActionUserStepType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.systemMessageAttributesType, TaskService.systemMessageAttributesType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.flexfieldMappingType, TaskService.flexfieldMappingType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.scaType, TaskService.scaType>(); AutoMapper.Mapper.CreateMap<TaskQueryService.UpdatableEvidenceAttributesType, TaskService.UpdatableEvidenceAttributesType>(); // check automapper config is valid AutoMapper.Mapper.AssertConfigurationIsValid(); } } }
Now let’s walk through and understand what is happening here. First we declare some variables that we will use. Notice that some of these are from the namespaces we just created with our web service references. We are going to be using the TaskQueryServiceClient and the workflowContextType (the credentials that we use to authenticate to the BPM server). We also will be using the task object. We define these at the class level as we want them to be avialable for the whole lifecycle of the page.
private string taskId; private string ctxId; private TaskQueryService.workflowContextType ctx; private TaskQueryService.TaskQueryServiceClient tqs; private TaskQueryService.task task;
As for the class, we are extending the class QueryStringRecipient and extending System.Web.UI.Page. We say partial class because this class is already defined elsewhere (in the .Net libraries) and we are just adding some extra logic to it. We use this class so that we can get access to the Query String from the HTTP Request.
public partial class QueryStringRecipient : System.Web.UI.Page
Now let’s take a look at the Page_Load method. This gets run (as you might guess) when the page is loaded. Every time the page is loaded – even post backs. So you have to be careful not to overwrite data from the user in here. This method is probably doing more work than it needs to on each page load the way I have it implemented – I guess you can optimise it some more 🙂 The first thing we need to do is set up the AutoMapper. This is done by calling a convenience method setupAutoMapper() which is down the bottom of the source file to hide all that ugliness. As I mentioned earlier, why and how we are using AutoMapper is discussed over here.
private void Page_Load(object sender, System.EventArgs e) { // setup the automapper setupAutoMapper();
Next, we need to read the data that BPM sends us in the Query String. When we configure a URI for a human task, like we did earlier in this post, BPM will append some data into the Query String for us. This is the data we want to read now:
- bpmWorklistTaskId is the taskId for the particular task instance we are interested in, and
- bpmWorklistConext is the BPM workflow context (security token) for the currently logged on user (logged on to BPM Workspace that is).
With these two pieces of information, we are able to do everything we need to do to that task – get its details, payload, take an action on it (system or custom actions), update it, etc. Here is the code to grab these from the Query String:
// BPM will pass us the taskID and the ctx token // need to read these out of the http request's query string taskId = Request.QueryString["bpmWorklistTaskId"]; ctxId = Request.QueryString["bpmWorklistContext"];
Now we are ready to start talking to BPM. We should first check that we actually got a task ID in the previous step, otherwise, we probably got called from outside of BPM Workspace, so we should just stop.
Then we can create our workflowContextType. To use the details that BPM sent us, all we need to do is put them into the token property of this type, as you see below. Then we are ready to create our TaskQueryServiceClient, note that we pass in to the constructor the name of the port we want – you wrote that down earlier, didn’t you? 🙂
// if this is running outside of the worklist, just exit if (taskId == null) { // looks like we dont have a task return; } // set up the BPM context ctx = new TaskQueryService.workflowContextType(); ctx.token = ctxId; // get the TQS tqs = new TaskQueryService.TaskQueryServiceClient("TaskQueryServicePort");
Next, we want to retrieve the task. We do this by calling the getTaskDetailsById() method on the TaskQueryServiceClient. First, we need to set up our inputs. This is done by creating a TaskQueryService.taskDetailsByIdRequestType and setting its workflowContext and taskId properties using the values we retrieved from the Query String earlier. Then we can call the method. We get back a task object.
// set up the request to get the task TaskQueryService.taskDetailsByIdRequestType getTaskRequest = new TaskQueryService.taskDetailsByIdRequestType(); getTaskRequest.workflowContext = ctx; getTaskRequest.taskId = taskId; // get the task task = tqs.getTaskDetailsById(getTaskRequest);
Now, let’s read the data out of the task and populate our UI components. Here we set the various text boxes to the task metadata we have chosen to show – title, status and task number.
Then we set the other group of text boxes (the ones for the payload) to those parts of the payload that we are interested in. Of course, we don’t need to display everything, just the fields we are interested in. In this example, we are going to take one of the two String fields from our dataObject1 and the String dataObject2. You can see that we access the payload through a System.ml.XmlNode[] – we can just cast the payload property of the task to this type and then we can easily read the payload data. You can see from the code here that this is specific to the payload type – so in this case we do need to know the structure of the payload. We could of course be a bit smarter and introspect the payload to find the data we want, but for now, hardcoding it will serve our purpose here.
Notice that we only want to populate the payload UI fields from the task payload the first time we load the page. That is why we check if (!Page.IsPostBack) – otherwise any changes that the user had made would be overwritten when they post back those very changes.
// populate the UI with task details TextBox1.Text = task.title; TextBox2.Text = task.systemAttributes.state.ToString(); TextBox3.Text = task.systemAttributes.taskNumber; if (!Page.IsPostBack) { // populate the UI with current payload data System.Xml.XmlNode[] payload = (System.Xml.XmlNode[])task.payload; TextBox4.Text = payload.ElementAt(0).ChildNodes.Item(1).InnerText; TextBox5.Text = payload.ElementAt(1).ChildNodes.Item(0).InnerText; }
That completes the Page_Load method. Now, let’s take a look at what happens when the user clicks on the OK button on our page. That button represents the OK (custom) action (or outcome) for that task.
The first thing we want to do is update the payload with the data that the user has entered in the form (if any). Note that only one of the two payload fields that we are displaying is editable – you may remember when we created our task that we set only one of the two parameters to be editable. That is why we are only taking the value from one of the TextBox components and updating the payload. If you tried to update the non-updatable field, you would get an exception (as you might expect.) Updating the payload is pretty mush the reverse of reading the payload:
protected void Button1_Click(object sender, EventArgs e) { System.Diagnostics.Debug.WriteLine("PRESSED OK"); // update the task payload from the UI System.Xml.XmlNode[] payload = (System.Xml.XmlNode[])task.payload; payload.ElementAt(0).ChildNodes.Item(1).InnerText = TextBox4.Text; task.payload = payload;
Now we have actually just updated the payload in our local copy of the task, we need to actually tell BPM to update the ‘real’ task on the server. We do this by calling the updateTask() method on the TaskService. Just like we did earlier for the TaskQueryService, we create an instance of the TaskService and pass in the name of the port we want to use.
Then we create the input for the updateTask() method, which is the oddly named TaskService.taskServiceContextBaseType. We can then populate it with the workflowContext we got from the Query String, and our newly updated task object.
Notice how we use the AutoMapper to convert between types in the two different namespaces created for our services.
The updateTask() method returns to us a new task object, which is represents the newly updated task object on the server. We will need to use this new object to take any further actions on this task, our old task object is now no longer of any use to us.
// get the TS TaskService.TaskServiceClient ts = new TaskService.TaskServiceClient("TaskServicePort"); // update task TaskService.taskServiceContextTaskBaseType updateTaskRequest = new TaskService.taskServiceContextTaskBaseType(); updateTaskRequest.workflowContext = AutoMapper.Mapper.Map<TaskQueryService.workflowContextType, TaskService.workflowContextType>(ctx); updateTaskRequest.task = AutoMapper.Mapper.Map<TaskQueryService.task, TaskService.task>(task); TaskService.task updatedTask = ts.updateTask(updateTaskRequest);
Finally, we can complete the task by setting the outcome to “OK” using the updateTaskOutcome() method on the TaskService. I won’t go through all the details, but you can see we create the input, populate it and the call the method.
Once this is done, we can redirect the browser to an empty page – just like the auto-generated ADF task forms do – so we maintain the normal user exerpience in the BPM Workspace. This is done with the Response.Redirect(“/Empty.htm”) call on the last line.
// complete task TaskService.updateTaskOutcomeType updateTaskOutcomeRequest = new TaskService.updateTaskOutcomeType(); updateTaskOutcomeRequest.workflowContext = AutoMapper.Mapper.Map<TaskQueryService.workflowContextType, TaskService.workflowContextType>(ctx); updateTaskOutcomeRequest.outcome = "OK"; updateTaskOutcomeRequest.Item = updatedTask; ts.updateTaskOutcome(updateTaskOutcomeRequest); // redirect to empty page Response.Redirect("/Empty.htm");
Now obviously we need to have such a page, so go ahead and create a new HTML page called Empty.htm and put the following code into it:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <body> </body> </html>
Ok, so that completes our .Net project. Go ahead and run itusing the ‘big green arrow’ icon. This will start up the embedded .Net server and run the project. It will probably open a browser window and show you the page. Just close this, we don’t need it.
Now, go and log in to BPM Workspace as an administrative user (like weblogic) and click on the Administration link in the top right corner. Select the role this task is assigned to, mine is DotNetTest.Role1 and then click on the little ‘new’ icon (highlighted in the image below) to assign some users to this role.
In the popup box, serach for weblogic and then move it across from Available to Selected. Then click on OK and then on Apply to save this new mapping. Now these tasks can be routed to the weblogic user.
Log out of the BPM Workspace and back in again so that it will refresh the mappings. You should now see the DotNetTest tasks that you created earlier in Enterprise Manager sitting in weblogic‘s queue. Click on one of them and your shiny new .Net task form will load. It should look a little like this:
Try it out! You can update the payload data, and then click on the OK button to action the task. After you do that, go take a look at the instance flow trace in Enterprise Manager and you can verify that the payload data was in fact changed.
So there you have it, a .Net task form fully integrated into the BPM Workspace application. Enjoy!
Thanks go to Carlos Casares for giving me the incentive to write this in the first place and to my reader in Saint Petersburg for giving me the incentive to publish it.
Mark this is truly amazing blog post i must say. And this is something that even i was starting to work on as i had a lot of people asking me whether they can reuse their existing custom JSP or .NET based UI’s for Oracle BPM 11g. Kudos!
Thanks Arun 🙂
Mark, Thanks a lot for posting this quickly as soon as I requested for it in the other thread. I appreciate it.
I am just about to try it. Could you clarify one thing, the web services (TaskQueryServicePort) that are specified in the web.config for the .NET application, they show /integration/services. Aren’t those meant for BPEL? I think there is still not web services interface for the BPM engine. So how does this work?? Is it that, because BPM is passing the context via:
taskId = Request.QueryString[“bpmWorklistTaskId”];
ctxId = Request.QueryString[“bpmWorklistContext”];
The calls to TaskQueryService actually go to BPM engine??
Thanks again for such a wonderful post!
These services are provided by the Human Workflow engine which is shared by BPEL and BPM.
That’s right. Got it now!Just occurred to me as well, as I posted the comment. It makes sense. Thanks for all the wisdom you share.
nice post
We wanted to acheive a similar behaviour by using a java based UI for a human task. The java web project used the Oracle Task Query Service and the Task service WSDLs (BPM human task is deployed on Oracle SOA PS5). The java web client is generated using Apache axis 1.4. When we deploy the java web project on tomcat and point the human task to use this GUI, we get the below exception:
org.xml.sax.SAXException: No deserializer for anyType.
We would like to know if you encountered a similar problem anytime during the .NET based implementation. Are there any suggestions to fix the same ?
Thanks.
I am guessing this is the payload where you got this issue? I did not have this issue with .Net, or with my Spring webmvc Java custom worklist. It might be to do with the way you are using axis…?
Hi,
Thanks for your suggestion.
We generated the stubs on JDeveloper using JAX-WS.
We are now able to do a getTask and then also call updateTask.
But when we call the updateTaskOutcome as illustrated in the below code sample, we get the exception (also provided below).
Can you please let us know if you have any inputs for this?
******Method********
public void updateTask( Task task) {
try {
taskService_Service =new TaskService_Service();
TaskService taskService = taskService_Service.getTaskServicePort();
//WorkflowContextType workflowContext;
TaskServiceContextTaskBaseType taskPayload=new TaskServiceContextTaskBaseType();
taskPayload.setWorkflowContext(this.cntxt);
taskPayload.setTask(task);
taskService.updateTask(taskPayload);
System.out.println(“Update success”);
UpdateTaskOutcomeType updateTask = new UpdateTaskOutcomeType();
updateTask.setWorkflowContext(this.cntxt);;
updateTask.setOutcome(“SUBMIT”);
updateTask.setTask(task);
taskService.updateTaskOutcome(updateTask);
System.out.println(“updateTaskOutcome success”);
} catch (Exception e) {
e.printStackTrace();
}
}
******Exception********
com.oracle.service.bpel.workflow.taskservice.WorkflowErrorMessage:
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstruct
orAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingC
onstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.sun.xml.internal.ws.fault.SOAPFaultBuilder.createException(SOAPFa
ultBuilder.java:113)
at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMetho
dHandler.java:240)
at com.sun.xml.internal.ws.client.sei.SyncMethodHandler.invoke(SyncMetho
dHandler.java:210)
at com.sun.xml.internal.ws.client.sei.SEIStub.invoke(SEIStub.java:103)
at $Proxy43.updateTaskOutcome(Unknown Source)
When you call updateTask, you get back a new task object that you need to use in future API calls to the TaskService, e.g.:
Task newTask = taskService.updateTask(taskPayload);
System.out.println(“Update success”);
UpdateTaskOutcomeType updateTask = new UpdateTaskOutcomeType();
updateTask.setWorkflowContext(this.cntxt);;
updateTask.setOutcome(“SUBMIT”);
updateTask.setTask(newTask);
taskService.updateTaskOutcome(updateTask);
System.out.println(“updateTaskOutcome success”);
Mark thanks for the Excellent article.. It helped me alot.
I have a doubt can you please kindly help me … Can we refresh the workspace from task form. I see that you are redirecting to an Empty webpage after the updating the task in the above post. But I have a requirement to refresh the tasklist once the task is updated or when the user exit the task. (I looking if there is a way to do in .NET similar to “CloseTaskFlow” in ADF).
Yeah that will happen automatically – the workspace app does that, so you just redirect to the empty.
I see automatic refresh is not happening with our existing BPM application.. Im brand new to Oracle BPM … so, can you please kindly give some sample/link to achive this in BPM.
Hi Mark, Our Oracle BPM workspace app is not refreshing automatically. So, can you please kindly let us know how we can refresh this from BPM side ( I brand new to BPM)… OR else is there any way that I can refresh from .Net side ?
Hi, the current version of the workspace does not have automatic refresh.
You can trigger a refresh using some cheesy javascript. It works by mimicking clicking the ‘Refresh’ icon on the My Tasks tab:
Task Complete
//<![CDATA[
function submitIt() {
if (window.opener == null) {
var allParentLinks = this.parent.document.getElementsByTagName("a");
for (var j = 0; j < allParentLinks.length; j++) {
var linkId = allParentLinks[j].getAttribute('id');
if (linkId != null && linkId.indexOf('refreshTaskList') != -1) {
this.parent.submitButton(linkId);
}
}
} else {
var allLinks = window.opener.document.getElementsByTagName("a");
for (var i = 0; i
…
Sorry – here’s the readable version
<html>
<head>
<title>Task Complete</title>
<script type="text/javascript">
//<![CDATA[
function submitIt() {
if (window.opener == null) {
var allParentLinks = this.parent.document.getElementsByTagName("a");
for (var j = 0; j < allParentLinks.length; j ) {
var linkId = allParentLinks[j].getAttribute('id');
if (linkId != null && linkId.indexOf('refreshTaskList') != -1) {
this.parent.submitButton(linkId);
}
}
} else {
var allLinks = window.opener.document.getElementsByTagName("a");
for (var i = 0; i < allLinks.length; i ) {
var windowLlinkId = allLinks[i].getAttribute('id');
if (windowLlinkId != null && windowLlinkId.indexOf('refreshTaskList') != -1) {
window.opener.submitButton(windowLlinkId);
}
}
}
window.close();
}
//]]>
</script>
</head>
<body onload="submitIt();">
</body>
</html>
Thanks for sharing Peter.
Thanks Peter for replying to my question…. I already did it in similar fashion only differene is:
Latest browers are not allowing Javascript to interact cross domain pages… (BPM is on weblogic and .Net Pages are on IIS)…so, I created an EMPTY.HTM page on BPM side and kept the JS code in the page load of that EMPTY.htm and Im redirect to that empty.htm page from .Net when where I want a BPM Workspace refresh.
It’s working fine…
Thanks Mark for the great post
i was wondering how to do what was mentioned in one of the comments which is
“I created an EMPTY.HTM page on BPM side” in order to auto refresh the task list after the workspace user submits the task?
i am very new to the whole bpm setup and linux and can’t figure it out
appreciate the great support
HI, thanks for your comment. You just need to create an HTML page in your .Net project – in the web root folder, called “empty.htm” with just enough html content in it so that it will render – like the stuff shown below for example. The worklist is going to do a redirect to “/empty.htm” after you have finished processing the task, so creating this “empty” file will cause the worklist to render a nice empty white space in the lower right pane, rather than a nasty 404 error 🙂
example – I cant put the proper html tags in the comment field so change parenthesis to less than/greater than..
(html)(head/)(body/)(/html)
Hi,
Below is the HTML for EMPTY.htm file .
I redirect to this file to referesh workspace after existing the task (I gave this file to my BPM team and asked them to deploy on weblogic along with BPM EAR)
function RefreshWindow() {
top.opener.document.getElementById(“wlwerc:j_id_id14:r1:0:testc:refreshTaskListButton”).click();
window.close();
return false
}
NOTE:- “wlwerc:j_id_id14:r1:0:testc:refreshTaskListButton” is the ID of the refresh icon on BPM workspace… you can get this by using viewsource on BPM workspace. But there should be a better way to get the refresh icon ID.. but this is good for my demo project.
Also.. the above javascript works when you open the task in a seperate window you might need to change the Javascript to make it work on the lower right panel in BPM workspace.
If you dont want to refresh workspace then Mark’s soultion is the best.
Invoke the above RefreshWindow function in the EMPTY.htm page load.
Example:-
(body onload= “RefreshWindow();”)
(/body )
Thanks for sharing that.
this is exactly what i am asking about. how can i do what reshwanths said “(I gave this file to my BPM team and asked them to deploy on weblogic along with BPM EAR)”
how do i do that (to deploy the file on weblogic bpm)?
i am fairly new to linux and i can’t figure out how to deploy the empty.html page?
This files goes in your .Net webapp – not on the BPM server.