Friday, August 27, 2010

InfoPath form and Workflow in SharePoint 2010 – Putting pieces together

To set up a InfoPath form as the instantiation form in a workflow in SharePoint 2010 is not that easy and it requires some manual setting here and there. What making it harder is the lack of documentation. Following is the details of the whole process:

 

Create and Publish a InfoPath form in InfoPath Designer 2010

There are tons of posts online walk you thru creating a workflow init InfoPath forms. After you created one, now there is a few steps you need to know about when it comes to publish.

First, you’ll need to set the security level of the form. It’s under File/Info/Advanced form options/Security and Trust. Set it to Domain.

Second, set the server validation under File/Info/Advanced form options/Compatibility.

Last, publish it to a Network Location which is a folder on your local machine. When it comes to the step that asks you for an alternate access path, leave it blank then click next. If you don’t leave it blank, you will get an error of can not access the form on the server.

image

You will also need to get the URN of the form for later use: it’s under File/Info/Form Template Properties (on the very right hand side)

image

 

Add and Set up the forms published above in Visual Studio

First, add the forms. To add the forms, the convenient way is to add a new Module item in the project, then add the forms under. The feature will automatically include this new added Module and all the files under it.

Notice that the path property of the forms is set to blank. It’s because we are going to use Microsoft.Office.InfoPath.Server.Administration.XsnFeatureReceiver as the feature event receiver which looks for InfoPath forms in the top level folder only.

Set the Forms Module feature receiver assembly to “Microsoft.Office.InfoPath.Server, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c”.

Set the Class Name to “Microsoft.Office.InfoPath.Server.Administration.XsnFeatureReceiver”.

image

 

Second, modify the Elements.xml under the Forms Module according to the following:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="Forms" Url="FormServerTemplates" RootWebOnly="TRUE">
<File Path="InitiationForm.xsn" Url="InitiationForm.xsn" Type="GhostableInLibrary" />
<File Path="TaskForm.xsn" Url="TaskForm.xsn" Type="GhostableInLibrary" />
</Module>
</Elements>

 

Third, modify the Elements.xml under the workflow folder which is CollectResponse in this example (use the form’s URN copied from above):

<?xml version="1.0" encoding="utf-8" ?>
 
<!-- Customize the text in square brackets. 
Remove brackets when filling in, e.g.
Name="[NAME]" ==> Name="MyWorkflow" -->
 
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Workflow
     Name="CollectResponseWorkflow - CollectResponse"
     Description="My SharePoint Workflow"
     Id="9bfcf279-c26d-48c1-91c6-6477bf3369ef"
     CodeBesideClass="WorkflowProject2.Workflow1.Workflow1" 
     InstantiationUrl="_layouts/IniWrkflIP.aspx" 
     ModificationUrl="_layouts/ModWrkflIP.aspx"
     CodeBesideAssembly="$assemblyname$">
    <Categories/>
    <MetaData>
      <AssociationCategories>List</AssociationCategories>
      <!-- Tags to specify InfoPath forms for the workflow; delete tags for forms that you do not have -->
      <!--<Association_FormURN>[c FOR ASSOCIATION FORM]</Association_FormURN>-->
      <Instantiation_FormURN>urn:schemas-microsoft-com:office:infopath:InitiationForm:-myXSD-2010-08-25T19-12-01</Instantiation_FormURN>
      <Task0_FormURN>urn:schemas-microsoft-com:office:infopath:TaskForm:-myXSD-2010-08-25T19-44-10</Task0_FormURN>
      <!-- Modification forms: create a unique guid for each modification form -->
      <!--<Modification_[UNIQUE GUID]_FormURN>[URN FOR MODIFICATION FORM]</Modification_[UNIQUE GUID]_FormURN>
      <Modification_[UNIQUE GUID]_Name>[NAME OF MODIFICATION TO BE DISPLAYED AS A LINK ON WORKFLOW STATUS PAGE</Modification_[UNIQUE GUID]_Name>
      -->
      <StatusPageUrl>_layouts/WrkStat.aspx</StatusPageUrl>
    </MetaData>
  </Workflow>
</Elements>

 

Last, modify the Feature1.Template.xml as shown in the following:

<Feature xmlns="http://schemas.microsoft.com/sharepoint/" Description="SharePoint Workflow Feature"
         Id="1f2088c0-3921-44ad-aa63-a531034b087f"
         ReceiverAssembly="Microsoft.Office.InfoPath.Server, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" 
         ReceiverClass="Microsoft.Office.InfoPath.Server.Administration.XsnFeatureReceiver" 
         Scope="Site" Title="CollectResponseWorkflow">
  <Properties>
    <Property Key="GloballyAvailable" Value="true" />
  </Properties>
  <ElementManifests>
    <ElementManifest Location="CollectResponse\Elements.xml" />
    <ElementManifest Location="Forms\Elements.xml" />
    <ElementFile Location="InitiationForm.xsn" />
    <ElementFile Location="TaskForm.xsn" />
  </ElementManifests>
</Feature>

 

Deploy your project and check the Form Templates in Central Admin

The two forms in this example are published with Workflow Enabled.

image

 

Tips

You might not be able to start a workflow after a second publish and need to manually enable it (there should be a way to avoid this).

Click Remove a workflow in the following screen

image

Click Allow

image

Friday, August 13, 2010

Add date time filters on custom entity picker in SharePoint 2010

Now you have created a custom entity picker which inherits from EntityEditorWithPicker in SharePoint 2010. By default, you have a dropdown and input box as for filters. However, you might have the request to add more filters on the picker to fit your business needs.

In this blog, I’m going to add two SharePoint DateTimeControls on the picker as shown below:

image

 

How do you achieve this?

First, you’ll need to override the CreateChildControls method in you custom QueryControl class which inherits from the SimpleQueryControl class and create the controls there, e.g. the SharePoint DateTimeControl in this example. Following is part of the the code.

protected DateTimeControl toDate;
protected DateTimeControl fromDate;
protected override void CreateChildControls()
{
    // add a table in here
    // . . . . . . 
    fromDate = new DateTimeControl();
    fromDate.AutoPostBack = true;
    fromDate.DateOnly = true;
    if (this.OpenDateFrom != null) fromDate.SelectedDate = (DateTime)this.OpenDateFrom;
    fromDate.ID = "fromDate";
    TextBox txtDateFrom = fromDate.FindControl(fromDate.ID + "Date") as TextBox;
    txtDateFrom.TextChanged += new EventHandler(fromDate_DateChanged);
    // . . . . . . 
    // add the controls
 
    base.CreateChildControls();
}

Note that there is an event handler on the text box which is part of the date time control. There are two reasons for doing this. First, the picker control doesn’t post back custom control value in the form submit and there is no way we can get the post back data unless we do a auto postback and catch it in the controls’s onchange event. Second, the event handler is on the text box which composes the date time control. This is because the date time control can’t catch the event when you remove the picked date time from the text box.

You can also add an UpdatePanel for better user interaction.

 

Following is the event handler:

void fromDate_DateChanged(object sender, EventArgs e)
{
    // parse the sender object to a textbox and save the value to ViewState
}

 

After we get the postback date time data, we need to save it in somewhere, like the ViewState, for use when user do a Search submit.

Last, filter your query with the saved date time data in the IssueQuery method. That’s it. Not hard.

Monday, August 9, 2010

Create custom workflow action in SharePoint 2010

There are two ways to create a workflow for SharePoint 2010, to use SharePoint Designer 2010 or to use Visual Studio 2010. To use SharePoint Designer 2010, you are under the limitation of the out of box actions that you can use. However, you are able the extend the actions limit by creating your own custom actions in Visual Studio 2010 and reuse them in SharePoint Designer 2010 in your custom workflow.
 
The out of box collect feedback workflow is very useful for some business needs and can be customized in SharePoint designer. But when it comes to the SendEmail action, SharePoint Designer doesn't support the Send From field and attachments. You’ll have to create your own custom actions to achieve these sort of requirements.
 
First, you'll need to create a new project SharePoint Activity Library under the Workflow template in Visual Studio 2010.
 
 
 
 
 
 
 
 
 
 
 
 





Rename the generated class Activity1.cs to what ever name you want, like CustomEmailAction.cs

 

Add two references: Microsoft.SharePoint and Microsoft.SharePoint.WorkflowActions
and the following two using statements:

using Microsoft.SharePoint;
using Microsoft.SharePoint.WorkflowActions;
using System.Text;
using System.Collections.Generic;

 
Add the DepedencyProperties for the Activity
public static DependencyProperty FromProperty = DependencyProperty.Register("From", typeof(string), typeof(CustomSendEmail));
public static DependencyProperty ToProperty = DependencyProperty.Register("To", typeof(ArrayList), typeof(CustomSendEmail));
public static DependencyProperty CCProperty = DependencyProperty.Register("CC", typeof(ArrayList), typeof(CustomSendEmail));
public static DependencyProperty SubjectProperty = DependencyProperty.Register("Subject", typeof(string), typeof(CustomSendEmail));
public static DependencyProperty BodyProperty = DependencyProperty.Register("Body", typeof(string), typeof(CustomSendEmail));
public static DependencyProperty WFContextProperty = DependencyProperty.Register("WFContext", typeof(WorkflowContext), typeof(CustomSendEmail));

 
And the corresponding Properties
[Description("Sender address. If this value is not specified, default sharepoint sender address will be used")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string From
{
get
{
return ((string)(base.GetValue(CustomSendEmail.FromProperty)));
}
set
{
base.SetValue(CustomSendEmail.FromProperty, value);
}
}

[Description("Recipient address")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public ArrayList To
{
get
{
return ((ArrayList)(base.GetValue(CustomSendEmail.ToProperty)));
}
set
{
base.SetValue(CustomSendEmail.ToProperty, value);
}
}

[Description("Carbon copy recipient")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public ArrayList CC
{
get
{
return ((ArrayList)(base.GetValue(CustomSendEmail.CCProperty)));
}
set
{
base.SetValue(CustomSendEmail.CCProperty, value);
}
}

[Description("Subject")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string Subject
{
get
{
return ((string)(base.GetValue(CustomSendEmail.SubjectProperty)));
}
set
{
base.SetValue(CustomSendEmail.SubjectProperty, value);
}
}

[Description("HTML body of the message")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string Body
{
get
{
return ((string)(base.GetValue(CustomSendEmail.BodyProperty)));
}
set
{
base.SetValue(CustomSendEmail.BodyProperty, value);
}
}

[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public WorkflowContext WFContext
{
get
{
return ((WorkflowContext)(base.GetValue(CustomSendEmail.WFContextProperty)));
}
set
{
base.SetValue(CustomSendEmail.WFContextProperty, value);
}
}
 
 
 
Add the Execute and two helper methods. The get list attachments and send email methods are not implemented which you can easily add your own.
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{

try
{
//need administrative credentials to get to Web Application Properties info
SPSecurity.RunWithElevatedPrivileges(delegate()
{

using (SPSite site = new SPSite(WFContext.Site.ID))
{
using (SPWeb web = site.AllWebs[WFContext.Web.ID])
{
string to = this.To == null ? string.Empty : ParseEmailAddress(this.WFContext, this.To);
string cc = this.CC == null ? string.Empty : ParseEmailAddress(this.WFContext, this.CC);
to = ProcessStringField(executionContext, to);
cc = ProcessStringField(executionContext, cc);
string from = ProcessStringField(executionContext, this.From);
string subject = ProcessStringField(executionContext, this.Subject);
string body = ProcessStringField(executionContext, this.Body);
SPList list=web.Lists[new Guid(this.WFContext.ListId)];
SPListItem listItem = list.Items.GetItemById(this.WFContext.ItemId);
// get the list attachments goes here
// send the email goes here
}
}
});

}
catch (Exception e)
{
throw new Exception(e.Message,e.InnerException);
}

return base.Execute(executionContext);
}

private string ProcessStringField(ActivityExecutionContext context, string str)
{
if (string.IsNullOrEmpty(str))
return string.Empty;

Activity parent = context.Activity;

while (parent.Parent != null)
{
parent = parent.Parent;
}

return Helper.ProcessStringField(str, parent, null);
}

private string ParseEmailAddress(WorkflowContext context, ArrayList curArray)
{
int num;
StringBuilder builder = new StringBuilder();
bool flag = false;
List<string> list = new List<string>();
for (num = 0; num < curArray.Count; num++)
{
if (curArray[num] == null)
{
list.Add(string.Empty);
}
else
{
string[] textArray = curArray[num].ToString().Split(new char[] { ';' });
foreach (string text in textArray)
{
list.Add(text);
}
}
}
for (num = 0; num < list.Count; num++)
{
if (list[num] != null)
{
if (!flag)
{
flag = true;
}
else
{
builder.Append(";");
}
builder.Append(Helper.ResolveToEmailName(context, list[num]));
}
}
return builder.ToString();
}


To deploy, you’ll need to create an Empty SharePoint Project.
Add a SharePoint Mapped Folder to {SharePointRoot}\Template\1033\Workflow
Add a xml file under the Workflow folder with the following content and rename it as Custom.SharePoint.Workflow.Actions. Replace the PublicKeyToken id with your id.
<?xml version="1.0" encoding="utf-8" ?>
<WorkflowInfo>
<Actions Sequential="then" Parallel="and">
<Action Name="Custom Send an Email"
ClassName="Custom.SharePoint.Workflow.Actions.CustomSendEmail"
Assembly="Custom.SharePoint.Workflow.Actions, Version=1.0.0.0, Culture=neutral, PublicKeyToken={your id}"
Category="Custom Actions"
AppliesTo="all">
<RuleDesigner Sentence="Custom Email %1">
<FieldBind Field="To,CC,Subject,Body" Text="these users" DesignerType="Email" Id="1"/>
</RuleDesigner>
<Parameters>
<Parameter Name="From" Type="System.String, mscorlib" Direction="Optional" DesignerType="StringBuilder"
Description="Sender of the email." />
<Parameter Name="To" Type="System.Collections.ArrayList, mscorlib" Direction="In" DesignerType="Person"
Description="Recipients of the email." />
<Parameter Name="CC" Type="System.Collections.ArrayList, mscorlib" Direction="Optional" DesignerType="Person"
Description="Carbon copy recipients of the email." />
<Parameter Name="Subject" Type="System.String, mscorlib" Direction="In" DesignerType="StringBuilder"
Description="Subject line of the email." />
<Parameter Name="Body" Type="System.String, mscorlib" Direction="Optional" DesignerType="StringBuilder"
Description="Body text of the email." />
<Parameter Name="WFContext" Type="Microsoft.SharePoint.WorkflowActions.WorkflowContext, Microsoft.SharePoint.WorkflowActions" Direction="In" DesignerType="Hide" />
</Parameters>
</Action>
</Actions>
</WorkflowInfo>

 
Double click Package.package to open the package setting form. Click on Advanced tab at the bottom and click on the Add button to add the custom send email assembly from the project
adddll

In Web.config, add the following authorizedType under System.Workflow.ComponentModel.WorkflowCompiler

<System.Workflow.ComponentModel.WorkflowCompiler>
<authorizedTypes>
<authorizedType Assembly="Custom.SharePoint.Workflow.Actions, Version=1.0.0.0, Culture=neutral, PublicKeyToken={your id}" Namespace="Custom.SharePoint.Workflow.Actions" TypeName="*" Authorized="True" />

Now it’s ready to deploy.