Saturday, September 20, 2008

MOSS Workflow History Best Practice

If you are designing a workflow solution in MOSS on of the first things you may think about incorporating into your solution is the Workflow History. The value proposition of using the Workflow History was:

  • When building workflows there is an activity you can drag right onto the process which will set values into the Workflow History table. It is really simple to use.
  • Users will be able to see all of the information immediately right there on the screen in the Workflow State page.

However after doing some more thinking and contacting a few of my colleagues I quickly decided not to use it to any great degree. Here are a couple of the issues that are with it.

  • First, the workflow history table will be cleaned up by SharePoint after a period of time. This time can be configured using the AutoCleanupDays however it will be run at some time. It will not actually delete the logs, but it will delete the log associations to the workflow instance and it will delete any tasks that are associated to the workflow instance.
  • Second, I got concerned with sizing of the workflow history list in general given the 2,000 item rule for a list. If you use it a lot you will quickly eclipse this and there can be performance issues. So the workflow history will have to have data deleted from it on a regular basis.
  • Third, in my opinion there is no real good way to search, query or do business reports of this data.
  • Fourth, since workflows can be removed from a list. Once the workflow is deleted, if you go back to the item the workflow instances will be deleted. This basically means to me that this data is not well managed.

The best practice in my opinion is to use the workflow history if you want to print out some quick information to the user on the workflow page or if you want to use it as a way to do some logging. It should not be used as a solution to satisfy auditing requirements. If you have that as a requirement, I would highly recommend dumping some data out to a custom database which can scale and is easier to query.

References:

Workflow Task Form Data Connections

Error

While I was working with a state workflow in SharePoint I ran into a problem with make secondary data connections my Task forms. I was getting the infamous "5566 An error occurred accessing a data source".

Background

Well Google was giving my tons of hits on the error but nothing helpful at all. I basically had a custom web service that I needed my Task form to use. Here was the situation:

  • I had a data connection a web service which I had converted and placed into a Connection Library in SharePoint.
  • Had changed the form properties to be Full Trust.
  • When I ran the preview of the InfoPath, the data connections were successful.
  • Here is where it gets interesting. I was able to deploy the form through central administration (because the form had full trust) and then associated the content type to a form library. When I opened the form as a web enabled form, it worked!

That last bullet completely confused me. Why would it work if I deployed the form manually but when the same InfoPath form was being used in a workflow it would not work?

Solution

After digging at it for a while this is what I found. Data connections that were executed through a rule would work however and data connections that were executed when the form was opened would not. For instance, I had a dropdown that I wanted to be loaded when the form was opened. So when I created the data connection on the last step of the wizard I set it to "Automatically retrieve data when the form is opened". My solution was to clear that checkbox and then go to Tools >> Form Options >> Open and Save >> and press the Rules button in the Open behavior section. I then added a Rule to query the data connection and then everything worked. Really strange…

Thursday, September 11, 2008

DelayActivity Not Working and Creating Simple Escalation

Scenario
Basically I want to start adding some escalations into my SharePoint state workflows. I want to send emails to people who have been assigned a task.

Error
When I put a Delay Activity into my state workflow, the delay activity would not work. No errors; no nothing. I put in some break points and the event definitely fires however it never wakes up to send the email.



Resolution
I did some searching and found out rather quickly this seems to be a well known error. Specifically this SharePoint team blog lead me to KB 953630 which discusses the issue. To resolve the issue I had to resolution 1 and 4. First I had to install KB 932394 which just required me to bounce both IIS and the SharePoint Timer service. Next I had to modify the Job-workflow timer using the Stsadm. Let's say you have a reminder that must be sent within two hours, if the Job-workflow time job is configured to go off once a day, your reminder will not be sent.

The reason why I had to install this KB is because my client has not installed the Infrastructure Update for Microsoft Office Servers (KB951297). If you have not installed it yet, please do so! There are a ton of good fixes.

Setting up the Escalation
So to implement an escalation into my state workflow, I simply added two Event Driven activities by right clicking the state. The first uses an OnTaskChanged activity which waits for a user action. The second is the escalation.

Now in the EscalationActivity, I simply put in a delay, an email and a log. Done.
Escalations in Thoughts
In K2, I do not even have to think about how to set up escalations but I had to figure out how to do in WF. First of all from what I read the DelayForActivity and DelayUntilActivities activities that you may see in the tool box have some challenges and work well in SharePoint Designer; not in Visual Studio fromw what I read. I knew I really did not need to use them, so I stuck just to DelayActivity.

I found these two blogs sending a reminder in a sequential workflow and using nested StateActivity to send regular reminders in Visual Studio SharePoint State Machine Workflow. Really they were overkill from what I found and the second article basically was hacking at the workflow to achieve a result where a user will continue to receive reminders until the person takes action. LET ME IMPLORE YOU; I would challenge any business user who claims this is a requirement for an everyday business process. I have seen way to many scenarios using workflow engines (at different client) where set up complex business rules and had expectations that users would be doing stuff in rapid fashion. Trust me, you are building a spam machine and the business users will hate you for it, especially if escalations are going up to the CEO. I HIGHLY recommend that you put in simple escalation rules or none at all until you can do some analysis of the workflow once it has been automated. Once you start collecting rich data about the process, you will identify bottlenecks and that is when you want to start introducing escalations.

Wednesday, September 10, 2008

WF Error Correlation Token Already Initialized

Error


This is another error I ran into which I found really interesting.


System.InvalidOperationException: Correlation value on declaration "managerTaskToken" is already initialized. at System.Workflow.Runtime.CorrelationToken.Initialize(Activity activity, ICollection`1 propertyValues) at System.Workflow.Activities.CorrelationService.InvalidateCorrelationToken(Activity activity, Type interfaceType, String methodName, Object[] messageArgs) at System.Workflow.Activities.CallExternalMethodActivity.Execute(ActivityExecutionContext executionContext) at System.Workflow.ComponentModel.ActivityExecutor`1.Execute(T activity, ActivityExecutionContext executionContext) at System.Workflow.ComponentModel.ActivityExecutor`1.Execute(Activity activity, ActivityExecutionContext executionContext) at System.Workflow.ComponentModel.ActivityExecutorOperation Run(IWorkflowCoreRuntime workflowCoreRuntime) at System.Workflow.Runtime.Scheduler.Run()


I did some and I found this rather quickly - http://social.msdn.microsoft.com/Forums/en-US/sharepointworkflow/thread/347537b5-7710-4576-98e9-fcd7b023ff0e/


Resolution


This error was really interesting to me and had to do with my lack of knowledge of the inner workings of WF. Here is a screenshot of an early iteration of sequential workflow I am creating.




In the workflow, the manager could reject back to the requestor and then the requestor will submit back to the manager for approval. Simple enough right? This error would occur after all of the resubmission events had completed and the state had gone back to the ManagerApproval state. What is happening is that I have different tasks created for each state which have their own tokens. To resolve the error make sure that on the CreateTask properties window that you select the name of the "state" for the OwnerActivityName and not the name of the "workflow" (in this case I select ManagerApproval). When the correlation token is set to the "state" it will be re-initialized when the state is created again (i.e. when the requestor resubmits back to the manager). The one thing to take note of is that once you do this the task cannot be reused in other states.

WF Error with Separate Association and Initiation Forms

Error

I came across another interesting error to today when building some workflows in Visual Studio for SharePoint.

Critical The form template failed to load. (User: SHAREPOINT\administrator, Form Name: InitiationForm, IP: , Request: http://mossserver:28921/_layouts/IniWrkflIP.aspx?List=0ffc0061-eb35-4533-80e8-2b488c4c8371&ID=1&TemplateID={611ca686-af64-497e-b3c4-debbb8421a56}&Source=http://mossserver:28921/createuer/Lists/aaa/AllItems.aspx, Form ID: urn:schemas-microsoft-com:office:infopath:InitiationForm:-myXSD-2008-09-09T17-43-22, Type: SchemaValidationException, Exception Message: Could not find schema information for the element 'http://schemas.microsoft.com/office/infopath/2003/myXSD/2008-09-09T15:06:55:myFields'.) 2b07a15f-6e26-440f-9ed2-351042351288

I did some searching and came up with this link which gave me a lead - http://social.msdn.microsoft.com/forums/en-US/sharepointworkflow/thread/83264f93-ebe3-49ec-bd6b-95ee02df4d8a/

Resolution

Well after re-reading my recommended book by Scot Hillier I noticed he had an obscure step where he took the Association InfoPath form he had created and then created the Initiation Form from it. Why he did that; well he did not explain other than saying the forms needed to be the same. Once I did this problem resolved. I then stumbled across this in my other readings which basically says the schemas of both of the forms must be the same.

I tried to do some more searching on why this is the case but I could not find anything. Really this dependency makes no sense what so ever. I really look at the association form as the form that is used to capture information related to the association of the workflow to the list while the initiation form would capture information that is only related to the workflow instance that is about to be created. An example would be that the association form must take a configuration to a database, web service address, or even a designed guid to another list that will be used by the workflow. While the Initiation form would capture such information as the manager's name that must approve. Right now, I have to combine all this into a single form.

WF Error: event receiver context for Workflow is invalid

Error

I ran into an interesting error when building up some WF State Machine Workflows for SharePoint. I had no problems building some sequential workflows and then I started building a fairly complex State Machine Workflow and I got a really useless error message that gave me no insight at all:

Value cannot be null

System.InvalidOperationException: The event receiver context for Workflow is invalid. at Microsoft.SharePoint.SPEventReceiverDefinition.ValidContext() at Microsoft.SharePoint.SPEventReceiverDefinition.ValidReceiverFields() at Microsoft.SharePoint.SPEventReceiverDefinition.GetSqlCommandToAddEventReceivers(IList`1 erds) at Microsoft.SharePoint.Workflow.SPWinOESubscriptionService.CommitNewSubscriptions(Transaction txn, IList`1 erds)

Error in commiting pending workflow batch items: System.InvalidOperationException: The event receiver context for Workflow is invalid. at Microsoft.SharePoint.SPEventReceiverDefinition.ValidContext() at Microsoft.SharePoint.SPEventReceiverDefinition.ValidReceiverFields() at Microsoft.SharePoint.SPEventReceiverDefinition.GetSqlCommandToAddEventReceivers(IList`1 erds) at Microsoft.SharePoint.Workflow.SPWinOESubscriptionService.CommitNewSubscriptions(Transaction txn, IList`1 erds) at Microsoft.SharePoint.Workflow.SPPendingWorkBatch.ProcessWorkItemBatch(Transaction transaction, Work method, IList`1 workItemBatch) at Microsoft.SharePoint.Workflow.SPPendingWorkBatch.Commit(Transaction transaction, ICollection items)

I did some searching and came up with some links but none of them helped:

Resolution

If you are getting this error basically you have somehow messed up the TaskID, TaskProperties, AfterProperties, BeforeProperties, or even your CorrelationTokens. When I say messed up, these values have probably not been initialized correctly.

In my case, I had forgot to do TaskId = Guid.NewGuid(); in one of my CreateTask event handlers and bam, I got this error.

Saturday, September 6, 2008

Creating Custom SmartObjects Services Part 2

Introduction


This is a second part to the first article I wrote some time ago on how to create a custom SmartObject Service and how to deploy it. It was completely based off Dynamic SQL Service located in the K2 blackmarket. The thing was the service was geared completely towards generating the interface based on the schema of a SQL Database. So I was lead down the path that I had to follow this pattern to define my SmartObject Service. Well I was playing around the other day and I saw a different project on the K2 blackmarket called the AD Interop. I read the code and I was flabbergasted. Apparently it is possible to create a regular class and then decorate the class, properties and methods with attributes to describe it. I dropped Colin an email saying to check this out and we both agreed this is a much cleaner way of building SmartObject Services which are not being generated dynamically.

I read through the AD Interop project and there were a few things which the project owner did not finish out like how to handle exceptions and how to properly pass configuration values from the ServiceBroker to the SmartObject Service. Below is a proof of concept I created for an employee SmartObject Service. Now it is not the most verbose implementation but should lead you in the right direction when trying to create a SmartObject Service.

Employee Object

First I actually went back to my first article and following my instructions for create a project. Then I created an Employee object. Notice it does not inherit from anything special. Notice the SourceCode.SmartObjects.Services.ServiceSDK.Objects.ServiceObject attribute which has been added to the class. It describes the Employee object.

using System;
using System.Collections.Generic;
using System.Text;
using SourceCode.SmartObjects.Services.ServiceSDK.Attributes;
using SourceCode.SmartObjects.Services.ServiceSDK.Objects;
using SourceCode.SmartObjects.Services.ServiceSDK.Types;

namespace CustomTestService
{

[ServiceObject("EmployeeServiceObject",
"Employee Service Object",
"This is a test Employee Service Object.")]
class Employee
{

}
}

Next I created some attributes and properties for the Employee object. Again nothing special. All of the properties are decorated with SourceCode.SmartObjects.Services.ServiceSDK.Objects.Property which describes name of the property, the type, a display name for the property and the description. The only property of any special interest is the DatabaseConnection property. This will only be used by the service broker. The service broker will set the database connection value that was set in the K2 workspace. Note that properties that are not exposed through a method are not visible in a SmartObject definition and cannot be set. This was a concern of mine because I did not want to expose this property publically.

        #region attributes

private string _databaseConnection = "";

private int _employeeNumber;
private string _department = "";
private string _firstName = "";
private string _lastName = "";
private string _email = "";

#endregion


#region properties

[Property("EmployeeNumber", SoType.Number,

"Employee Number", "Employee Number of employee")]

public int EmployeeNumber {

get {

return _employeeNumber;

}

set {

_employeeNumber = value;

}

}



[Property("Department", SoType.Text,

"Department", "Department of employee")]

public string Department

{

get

{

return _department;

}

set

{

_department = value;

}

}



[Property("FirstName", SoType.Text,

"First Name", "First Name of employee")]

public string FirstName {

get {

return _firstName;

}

set {

_firstName = value;

}

}



[Property("LastName", SoType.Text,

"Last Name", "Last Name of employee")]

public string LastName {

get {

return _lastName;

}

set {

_lastName = value;

}

}



[Property("Email", SoType.Text,

"Email", "Email of employee")]

public string Email {

get {

return _email;

}

set {

_email = value;

}

}



/// <summary>

/// Note this property was created to pass a value from the

/// EmployeeServiceBroker to the Employee object. This property

/// is not exposed through any of the methods.

/// </summary>

[Property("DatabaseConnection", SoType.Text,

"Database Connection", "Connection string to the database.")]

public string DatabaseConnection

{

get

{

return _databaseConnection;

}

set

{

_databaseConnection = value;

}

}

#endregion

Finally I created three methods: GetEmployee, HireEmployee, and GetEmployees. These are stubs and with no real implementation. Notice that each method is decorated with SourceCode.SmartObjects.Services.ServiceSDK.Objects.Method which takes the name of the method, the method type, a display name, and description. As well, there are three arrays which define the required fields, input fields and output fields. Notice the methods always return itself and any property that is defined as an input field to the method will have a value populated into it which can be used to perform operations (like searching for an employee based on their employee number). I was really happy to see I was able to easily return a collection of Employee objects for a List operation.


        #region Methods



/// <summary>

/// This method will get an Employee based on the Employee Number.

/// The Employee number is required.

/// </summary>

/// <returns></returns>

[Method("GetEmployee", MethodType.Read, "Get Employee",

"Method will get Employee based on Employee ID.",

new string[] {"EmployeeNumber"},

new string[] {"EmployeeNumber"},

new string[] {"FirstName", "LastName", "Email", "Department"})]

public Employee GetEmployee()

{

try

{

///Write code here to fill in the object from where ever.

///this.EmployeeNumber; will contain the employee number

///value that was sent by the caller.



///Filling in some test data.

FirstName = "Jason";

LastName = "Apergis";

Department = "Professional Services";

Email = "jason@foobar.com";

}

catch (Exception ex)

{

throw new Exception("Error Getting an Employee >> " + ex.Message);

}



return this; //return the Employee object to the Execute() call

}



/// <summary>

/// This method will create an Employee. First name, last name

/// and department are required. An employee number and email

/// address will be generated as a result of the creation of the employee.

/// These two values will be returned.

/// </summary>

/// <returns></returns>

[Method("HireEmployee", MethodType.Execute, "Hire Employee",

"Method will complete the hire of an email creating a Employee Number and Email Address.",

new string[] {"FirstName", "LastName", "Department"},

new string[] {"FirstName", "LastName", "Department"},

new string[] {"EmployeeNumber", "Email"})]

public Employee HireEmployee()

{

Employee employee = new Employee();



try {

///Write code here to save the object to where ever.

///this.FirstName, etc. to get values provided by the caller.



///returning some test values

EmployeeNumber = 1;

Email = "jason@foobar.com";

}

catch (Exception ex)

{

throw new Exception("Error Hiring an Employee >> " + ex.Message);

}



return this; //return the Employee object to the Execute() call

}



/// <summary>

/// This method will get a list of employees. Providing no value

/// will get all employees. Providing a Department name will get

/// all employees for the department. This is because the Department

/// field is not required.

/// </summary>

/// <returns></returns>

[Method("GetEmployees", MethodType.List, "Get Employees",

"Method will get Employees.",

new string[] { },

new string[] {"Department" },

new string[] {"EmployeeNumber", "FirstName", "LastName", "Email" })]

public List<Employee> GetEmployees()

{

List<Employee> employees = new List<Employee>();



try

{

///Check of there is a Department value

if (String.IsNullOrEmpty(Department))

{

///Get all Employees

}

else {

///Get Employees based on department id

}



///Filling in some test data.

Employee emp1 = new Employee();

emp1.EmployeeNumber = 0;

emp1.FirstName = "Jason";

emp1.LastName = "Apergis";

emp1.Department = "Professional Services";

emp1.Email = "jason@foobar.com";



employees.Add(emp1);



Employee emp2 = new Employee();

emp2.EmployeeNumber = 1;

emp2.FirstName = "Ethan";

emp2.LastName = "Apergis";

emp2.Department = "Professional Services";

emp2.Email = "ethan@foobar.com";



employees.Add(emp2);

}

catch (Exception ex)

{

throw new Exception("Error Getting Employees >> " + ex.Message);

}



return employees; //return the Employee objects to the Execute() call

}



#endregion

Now below is the EmployeeServiceBroker which inherits from ServiceAssemblyBase. In the GetConfigSection() override is where the DatabaseConnection service configuration is defined. When the service instance is created in the K2 Workspace, the administrator will be required to set a connection string which will be associated to the specific service instance. In the DescribeSchema() method notice that the Employee object is add the Service's SmartObjects list. Unlike my first article, nothing more needs to be done in this method because the definition is on the Employee object. Finally I had to override the Execute() method. The Execute() method actually does not have to be overridden, however I am overriding it so that I can pass the database connection string for the SmartObject service instance to the Employee object. As well, I wanted to properly send exception messages using the ServicePackage.

using System;

using System.Collections.Generic;

using System.Text;

using SourceCode.SmartObjects.Services.ServiceSDK;

using SourceCode.SmartObjects.Services.ServiceSDK.Objects;

using SourceCode.SmartObjects.Services.ServiceSDK.Types;

using System.Data;

using System.Data.SqlClient;



namespace CustomTestService

{

public class EmployeeServiceBroker : ServiceAssemblyBase

{

public EmployeeServiceBroker()

{



}



public override string GetConfigSection()

{

this.Service.ServiceConfiguration.Add("DatabaseConnection", true, "Default Value");



return base.GetConfigSection();

}



public override string DescribeSchema()

{

//Custom service object

Type employeeType = typeof(Employee);

base.Service.ServiceObjects.Add(new ServiceObject(employeeType));



return base.DescribeSchema();

}



/// <summary>

/// This method does not have to be implemented. However it is used to set

/// configuration values that are set in the K2 Workspace. As well, exceptions

/// from the Employee object are handled here.

/// </summary>

public override void Execute()

{

try

{

foreach (ServiceObject so in base.Service.ServiceObjects)

{

if (so.Name == "EmployeeServiceObject")

{

string server = base.Service.ServiceConfiguration["DatabaseConnection"].ToString();

so.Properties["DatabaseConnection"].Value = server;

}

}



base.Execute();

}

catch (Exception ex) {

string errorMsg = Service.MetaData.DisplayName + " Error >> " + ex.Message;

this.ServicePackage.ServiceMessages.Add(errorMsg, MessageSeverity.Error);

this.ServicePackage.IsSuccessful = false;

}

}



public override void Extend()

{

//throw new Exception("The method or operation is not implemented.");

}



}

}

Next all I needed to do was deploy the SmartObject service which I describe how to do here.


Employee SmartObject

Now that I have my Employee SmartObject Service deployed, I needed to create an Employee SmartObject. The following is a screenshot of a quick SmartObject that I threw together.


Test Stub

Finally I created a little command line application to test the SmartObject and Service that I had created. I added references to SourceCode.SmartObjects.Client and SourceCode.Hosting.Client and I was off and running.

using System;

using System.Collections.Generic;

using System.Text;

using SourceCode.SmartObjects.Client;

using SourceCode.Hosting.Client.BaseAPI;



namespace TestSO

{

class Program

{

static void Main(string[] args)

{

SmartObjectClientServer server = new SmartObjectClientServer();



try

{

SCConnectionStringBuilder cb = new SCConnectionStringBuilder();

cb.Host = "BLACKPEARL";

cb.Port = 5555;

cb.Integrated = true;

cb.IsPrimaryLogin = true;



//Connect to server

server.CreateConnection();

server.Connection.Open(cb.ToString());



//--------------------------

//Hire the Employee

//Get SmartObject Definition

SmartObject newEmployee = server.GetSmartObject("CustomTestEmployee");



//Hire Employee

newEmployee.Properties["FirstName"].Value = "Jason";

newEmployee.Properties["LastName"].Value = "Apergis";

newEmployee.Properties["Department"].Value = "Professional Services";



//Hire the employee

newEmployee.MethodToExecute = "HireEmployee";

server.ExecuteScalar(newEmployee);



//values returned

System.Diagnostics.Trace.WriteLine(

newEmployee.Properties["ID"].Value);



System.Diagnostics.Trace.WriteLine(

newEmployee.Properties["Email"].Value);



//--------------------------

//Get the Employee

//Get SmartObject Definition

SmartObject employee = server.GetSmartObject("CustomTestEmployee");



//Set properties

employee.Properties["ID"].Value = "1";



//Get the record

employee.MethodToExecute = "GetEmployee";

server.ExecuteScalar(employee);



System.Diagnostics.Trace.WriteLine(

employee.Properties["FirstName"].Value);



System.Diagnostics.Trace.WriteLine(

employee.Properties["LastName"].Value);



System.Diagnostics.Trace.WriteLine(

employee.Properties["Email"].Value);



//--------------------------

//Get employees using a Department Name

//Get SmartObject Definition

employee = server.GetSmartObject("CustomTestEmployee");



//Set properties

employee.Properties["Department"].Value = "Professional Services";



//Get the records

employee.MethodToExecute = "GetEmployees";

SmartObjectList employees = server.ExecuteList(employee);



//Loop over the return employee values

foreach (SmartObject so in employees.SmartObjectsList) {

foreach (SmartProperty property in so.Properties) {

System.Diagnostics.Debug.Write(

property.Name + "=" + property.Value);

}

}

}

catch (Exception ex)

{

throw new Exception("Error Creating Request >> " + ex.Message);

}

}

}

}

Conclusions


I was extremely happy with how clean this was. I was able to create an Employee object, decorate it and then it was deployed as a Service which K2 or other applications across my enterprise can use. The implementation is extremely decoupled and I had to put no thought to it at all. Going down this path, it is possible to re-use Domain Layers that may already be written and expose them to the enterprise on the large.