Starting Integrated (app) workflow from Host web

It has been a while since I last posted to this blog, for various reasons of course such as time and simply not being involved to much in SharePoint Development over the last years, or at least, not on the complex side of things ūüėČ.

Now I have stumbled across another challenge where unfortunately I was not able to find much information about online, so decided to put this one online myself. I hope it will help someone who encounters the same issue.

 

Launching integrated workflow from host web

First off, what is an integrated workflow? Well, since a while, MS supports workflows being added through a SharePoint Add-In to be connected to a list on the host web, thus the workflow being integrated. These are usually Visual Studio Workflows, but could also be declaritive workflows though. That being said, they are based on the SP 2013 workflow platform.

Recently I wanted to launch such a workflow through scipt, because it should be launched on any item event, but instead, on the discretion of the user. Of course, he could navigate all the way to the workflow start screen, but in this scenario, I needed to have it started on a button click. Fortunately, the JSOM framework provides a workflow services manager which can kick those off for you. Sounds simple right? You have a list, which has workflows attached. You then just get all subscriptions, pick the one you want and launch it. Well, for SharePoint 2013 workflows (designer), that is indeed how it works. But for the integrated workflows, i.e. the ones which are attached through a SharePoint hosed app (or provider hosted for that matter), things are slightly different.

So, I will first start with how we would do it for SP 2013 workflows:

Add the following scripts tags to your page:

<script type="text/javascript" src="/_layouts/15/sp.js">
<script type="text/javascript" src="/_layouts/15/sp.core.js">
<script type="text/javascript" src="/_layouts/15/sp.runtime.js">
<script type="text/javascript" src="/_layouts/15/sp.workflowservices.js">

We can then lookup the subscription and start the workflow, assuming we know the subscriptionID.
If we do not, the workflowSubscriptionService can also return a collection which we can enumerate to find the one you want.

function startWorkflow(itemID, subID) {
var
context = SP.ClientContext.get_current();
var
web = context.get_web();
var
wfServiceManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, web);
var
subscription = wfServiceManager.getWorkflowSubscriptionService().getSubscription(subID);
context
.load(subscription);
context
.executeQueryAsync(function(sender, args) {
console
.log(“Loaded subscription. Starting workflow.”);¬†¬†¬†¬†¬†¬†¬†
      var inputParameters = {};
wfServiceManager
.getWorkflowInstanceService().startWorkflowOnListItem(subscription, itemID, inputParameters);
context
.executeQueryAsync(function(sender, args) {
console
.log(“Started workflow.”);
      },  function(sender, args) {
console.log(“Cannot start workflow.”);
console
.log(“Error: “ + args.get_message() + “\n” + args.get_stackTrace());
}
);
},
function(sender,args) {
console
.log(“Cannot get subscription.”);
console
.log(“Error: “ + args.get_message() + “\n” + args.get_stackTrace());
}
);
}

So pretty straight forward:

  • Obtain the client context by getting ClientContext.get_current()
  • Then get the services manager for the workflows.
  • Obtain the subscription (association) though the manager passing the subscription ID.
  • Finally, when found, start the workflow by calling startWorkflowForListItem on the workflow service, passing the subscription, the item ID of the list item and the initiation parameters

Ok, so far so good. But when I pass the subscription ID of an integrated workflow, that is, a workflow that is added through a SharePoint app, this will fail.
It will not be able to start the workflow as it cannot find that subscription.

The reason for this is as straightforward as frustrating to find. This is because the subscriptions are not defined in the Host web, but in the App web of the SharePoint app. Now it would be nice if MS also returned the integrated workflow subscriptions from the workflow services manager, but unfortunately it does not. So how do we go about it?

Well, instead of using the Host context, which we obtain by calling SP.ClientContext.get_current(), I use the context of the App web. The only problem is that the app web will be deployed to a different app web moving from one environment to another, so I needed to have something more stable.

The following is therefore how to obtain a context to the app web. The rest of the code can remain the same ūüėČ, so let‚Äôs first look at how to get the app web context.

var appInstances = SP.AppCatalog.getAppInstances(clientContext, web);
clientContext.load(web);
clientContext.loadQuery(appInstances);
clientContext.executeQueryAsync( function () {
    if (appInstances.get_count() > 0) {
        for (var i = 0; 1 < appInstances.get_count() ; i++)  {
            var v = appInstances.getItemAtIndex(i);
if (v.get_title() == ‚Äúyour app name‚ÄĚ) ¬†{¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† ¬†
                 var url = v.get_appWebFullUrl();
}  //if
         }  //for
     }  // if
 } , function (sender, args) {
alert(JSON.stringify(args));
});

Please note that if your workflows are in a provider hosted app, the v.get_AppWebFullUrl will return null and instead, v.get_remoteAppUrl will return the url to the app.

So now we have the url of the App web in the url variable.
So we can use it to create a new ClientContext based on that url.

var context = new SP.ClientContext(url);

Now that we have that we can use the same lines of code as originally used, but using the different context instead.

So, putting it all together:

function StartWorkflow(itemId, workflowName) {
// get current context
var clientContext = new SP.ClientContext.get_current();

var appInstances = SP.AppCatalog.getAppInstances(clientContext, web);
clientContext.load(web);
clientContext.loadQuery(appInstances);

clientContext.executeQueryAsync(function () {
if (appInstances.get_count() > 0)
{
for (var i = 0; 1 < appInstances.get_count() ; i++)
{
var v = appInstances.getItemAtIndex(i);
if (v.get_title() == ‚Äúyour app name‚ÄĚ) {
var url = v.get_appWebFullUrl();
              StartAppWorkflow(itemId, workflowName, url);
}
}
}
}, function (sender, args) {
alert(JSON.stringify(args));
});
}

function StartAppWorkflow(itemId, workflowName, appweburl) {
// setup context to the app web
context = new SP.ClientContext(appweburl);
factory = new SP.ProxyWebRequestExecutorFactory(appweburl);
context.set_webRequestExecutorFactory(factory);
var web = context.get_web();
context.load(web);
context.executeQueryAsync(function() {
wfsManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context,web),
wfSubscriptions = wfsManager.getWorkflowSubscriptionService().enumerateSubscriptions();
context.load(wfSubscriptions);
wfsManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context,web),
wfSubscriptions = wfsManager.getWorkflowSubscriptionService().enumerateSubscriptions();
context.load(wfSubscriptions);
context.executeQueryAsync(function () {
var wfsEnum = wfSubscriptions.getEnumerator();
while(wfsEnum.moveNext()) {
var wfSubscription = wfsEnum.get_current();
if(wfSubscription.get_name() == workflowName) {
wfsManager.getWorkflowInstanceService().startWorkflowOnListItem(wfSubscription,itemId,new Object());
context.executeQueryAsync(function() {
var note = SP.UI.Notify.addNotification(‘Started Workflow: ‘ + workflowName + ‘ on item: ‘ + itemId, false);
}, function() {
alert(‘Could not start workflow.’);
});
}
}
});
});
}

A special note needs to be made here. SharePoint 2016 is more secure and adheres to Cross Site Scripting rules (CORS). As the app web resides in a different domain than the host web, it will throw an exception violating CORS (401 unauthorized) when trying to execute against the app web context.

To circumvent, we install a proxy to execute the web requests, by calling:

factory = new SP.ProxyWebRequestExecutorFactory(appUrl);
context.set_webRequestExecutorFactory(factory);

This overrides the web request to take the one from the proxy.

That’s it! You should be able to start all integrated workflows now.

Advertisements

Upgrading SP 2010 solutions to SP 2013 with resources in _layouts

As SharePoint 2013 is making it way to the developers, solutions get upgraded to SharePoint 2013.

One of the things people encountered is issues with javascripts and images when upgrading their solutions.
Thanks to Keith Tuomi, I will post the source of the problems here.

SharePoint 2013 offers the ability to be compatible with SharePoint 2010. In short, this means that a lot of features are ‘double’, one of which is the special reference in IIS to the _layouts folder.

In SharePoint 2013, the reference to the 15 hive is “_layouts/15” and NOT “_layouts”. Instead, “_layouts” points to the 14 hive.

In short, if you have resources and code relying on server side code and mappings, make sure you update the references to these files to point to the correct virtual directory!

Thanks to Keith for this information.

Co-writers wanted

Keeping a blog requires quite some time to keep up with the trends and technologies. Someone suggested in the past to co-write on my blog.

Consider this as an open sollicitation ūüėȬ† Obviously, knowledge about SharePoint 2010/2013 is required, but not all articles have to be purely technical. I am also looking to expand the articles to more IT pro related posts, as well as trending technologies like Azure and Windows 8.1 applications in combination with SharePoint.

If you think you have info to share that is worth reading and you would like to participate on this blog by writing articles, please drop me an email with your credentials, ideas and what you think you can contribute.

Obviously, your name will be published as an author on this site, as well as your own profile page.
Thanks!

Workflow activity: Set Managed Metadata column

Sometimes you wonder why certain things are not possible or why Microsoft did not included that in their shipping of the product. On the other hand, it makes sure we also have a job to do ūüėČ
One of those things is the fact that the out-of-the-box version of SharePoint 2010 seems to miss certain workflow actions that seem logical. This blog post covers one of those, the in-ability to set a Managed Metadata column.

In my current project, we encountered this behaviour when trying to create workflows that modify mms columns. Out of the box, the only way available is to use either the Update List Item or Set Field actions.
In both cases however, you need to provide the exact string for the MMS value, in the form of <id>;<value>, i.e. 34;My Value. Not ideal as this could be different here and there and could change in the future. More importantly though, it does not work in all cases. It seems to work fine when no value was set for the column yet, but as soon as it is, you are not able to modify it from the workflow.

So I decided to develop a small custom action to overcome this problem and enable us to update MMS columns, whatever their current value is. In this post, I will not go over the entire setup of developing a custom action. Please review my other post on email activities for more info on that. Instead, I will only cover the code needed specifically for this action.

Actions file

     <Action Name=Set Managed Metadata Column
             ClassName=$SharePoint.Project.FileNameWithoutExtension$.SetMMSColumn

Assembly=$SharePoint.Project.AssemblyFullName$

AppliesTo=all

Category=List Actions>
      <RuleDesigner Sentence=Update %1 with %2 from %3 and %4>
        <FieldBind Field=ColumnName Text=this column DesignerType=TextArea Id=1/>
        <FieldBind Field=TermName Text=this term DesignerType=TextArea Id=2/>
        <FieldBind Field=TermSetName Text=this termset DesignerType=TextArea Id=3/>
        <FieldBind Field=GroupName Text=this group DesignerType=TextArea Id=4/>
      </RuleDesigner>
      <Parameters>
        <Parameter Name=__Context Type=Microsoft.SharePoint.WorkflowActions.WorkflowContext Direction=In />
        <Parameter Name=__ListId Type=System.String, mscorlib Direction=In />
        <Parameter Name=__ListItem Type=System.Int32, mscorlib Direction=In />
        <Parameter Name=__ActivationProperties Type=Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties, Microsoft.SharePoint Direction=Out />
        <Paramater Name=ColumnName Type=System.String, mscorlib Direction=In/>
        <Paramater Name=TermName Type=System.String, mscorlib Direction=In/>
        <Paramater Name=TermSetName Type=System.String, mscorlib Direction=In/>
        <Paramater Name=GroupName Type=System.String, mscorlib Direction=In/>
      </Parameters>
    </Action>

As you can see above, the action defines an action with 4 parameters, the name of the MMS column, the name of the term, the name of the termset and finally the name of the group. The other 4 parameters are default parameters that allow you to get the context of the workflow.

Now let us move on to the code of the action.

       #region [ Custom Workflow Properties ]

¬†¬†¬†¬†¬†¬†¬† public static DependencyProperty ColumnNameProperty = DependencyProperty.Register(“ColumnName”, typeof(string), typeof(SetMMSColumn));
        [ValidationOption(ValidationOption.Required)]
        public string ColumnName
        {
            get
            {
                return ((string)(base.GetValue(ColumnNameProperty)));
            }
            set
            {
                base.SetValue(ColumnNameProperty, value);
            }
        }

public static DependencyProperty TermNameProperty = DependencyProperty.Register(“TermName”, typeof(string), typeof(SetMMSColumn));
       [ValidationOption(ValidationOption.Required)]
       public string TermName
        {
           get
            {
                return ((string)(base.GetValue(TermNameProperty)));
            }
            set
            {
                base.SetValue(TermNameProperty, value);
            }
        }

public static DependencyProperty TermSetNameProperty = DependencyProperty.Register(“TermSetName”, typeof(string), typeof(SetMMSColumn));
       [ValidationOption(ValidationOption.Required)]
       public string TermSetName
        {
            get
            {
                return ((string)(base.GetValue(TermSetNameProperty)));
            }
            set
            {
                base.SetValue(TermSetNameProperty, value);
            }
        }

¬†¬†¬†¬†¬†¬†¬† public static DependencyProperty GroupNameProperty = DependencyProperty.Register(“GroupName”, typeof(string), typeof(SetMMSColumn));
        [ValidationOption(ValidationOption.Required)]
        public string GroupName
        {
            get
            {
                return ((string)(base.GetValue(GroupNameProperty)));
            }
            set
            {
                base.SetValue(GroupNameProperty, value);
            }
        }

#endregion
[ Custom Workflow Properties ]

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            using (var web = __Context.Web)
            {
                   try
                   {
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† Common.AddCommentWorkflowHistory(“Start trying to set MMS column”, executionContext, WorkflowInstanceId);
                    // get column to update
                    SPList list = web.Lists.GetList(new Guid(__ListId), true);
                   SPListItem item = list.GetItemById(__ListItem);
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† Common.AddCommentWorkflowHistory(“Obtained list and item references”, executionContext, WorkflowInstanceId);
                   // get MMS column
                   TaxonomyField field = (TaxonomyField)list.Fields[ColumnName];
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†Common.AddCommentWorkflowHistory(“Obtained list, item and column references”, executionContext, WorkflowInstanceId);
                   //Get the Guid for the Term Store
                   Guid termStoreID = field.SspId;
                   TaxonomySession session = new TaxonomySession(web.Site);
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† Common.AddCommentWorkflowHistory(“Taxonomy session created”, executionContext, WorkflowInstanceId);
                   // Get group, termset and term
                   TermStore store = session.TermStores[termStoreID];
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†Common.AddCommentWorkflowHistory(“TermStore reference created”, executionContext, WorkflowInstanceId);
                   // Get Group, Set abd Term
                   Group group = store.Groups[GroupName];
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† Common.AddCommentWorkflowHistory(“Found group”, executionContext, WorkflowInstanceId);
                   TermSet set = group.TermSets[TermSetName];
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† Common.AddCommentWorkflowHistory(“Found TermSet”, executionContext, WorkflowInstanceId);
                   Term term = set.Terms[TermName];
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† Common.AddCommentWorkflowHistory(“Found Term”, executionContext, WorkflowInstanceId);
                   // Set value
                   field.SetFieldValue(item, term);
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† Common.AddCommentWorkflowHistory(“Updated column”, executionContext, WorkflowInstanceId);
                   item.Update();
                }
               catch (Exception ex)
                {
                   // Log entry to Workflow History Log
¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬†¬† Common.WriteFailToHistoryLog(web, WorkflowInstanceId, string.Format(CultureInfo.InvariantCulture, “Failed to set MMS column. Error message: {0}”, ex.Message));
                   throw;
               }
               finally
               {
                   // Cleanup РDispose of these private properties before exiting
                  if (__ActivationProperties != null)
                  {
                       __ActivationProperties.Dispose();
                  }
                  if (__Context != null)
                  {
                      __Context.Dispose();
                  }
                }
            }
           return ActivityExecutionStatus.Closed;
       }

Now the real meat to the bone is in the ActivityExecutionStatus method that will be called by our workflow on start. We will first obtain references to the list and current item before proceeding. Once done, we get the MMS column from the item and cast it to a TaxonomyField. We can then obtain the configured TermStore for that column through the field reference.

We then setup a TaxonomySession and obtain a reference to the TermStore. From that point on, easy to get the Group, TermSet and Term required for the update. We finally update the item using the SetFieldValue method from the TaxonomyField.

This simple action can be extended by allowing parameters from the start of the action to allow the end-user to enter the appropriate term, set and group on start of the workflow. That is however beyond the scope of this blog.

Couple of things you should be aware of.

  1. When no TermStores are returned by the session object, please ensure that your MMS service proxy is added to the default proxy group using the following PowerShell command:

    Set-SPMetadataServiceApplicationProxy¬†-Identity¬†“<your MMS name>”¬†‚ÄďDefaultProxyGroup

  2. If your activity does not show up in SharePoint designer, please check if you created the appropriate language folder in your project for the ACTIONS file. Misplacing this file will cause the activity not to show up.
  3. The session object is not very consistent. I found it more reliable to use the term store ID from the taxonomy field to request the TermStore from the session object.
  4. When deployed, run the workflow a couple of times. It takes a while for the connections to be setup and running the workflow directly after deployment might cause an exception in finding the TermStore.
  5. I suspect the reason why we cannot update a MMS column from a workflow using the OOB actions is because the link to the TermStore fails in doing the update. I have not reveived the OOB code using Reflector or something, but it could explain why it is not posisble.


Hope this fills a gap in your requirements!

Update: I have updated the code of the email activity to include this activity too. Download the sample solution here!

SharePoint Conference 2012 Las Vegas: Thoughts and Experiences

What happens in Vegas, stays in Vegas. A common used phrase around here to reflect the joy and possibilities of this city. This years SharePoint Conference was held at the Mandalay Bay Resort & Casino in Las Vegas.

This years conference also marked the beginning of a new era, as Microsoft launched the new version of SharePoint: SharePoint 2013! In 4 days time, Microsoft brought around 10.000 developers, enthousiast, IT Pros and Business Decision makers up to speed on the road ahead, the new features and the direction they are taking with their products. And believe me, things are going to change!

Microsoft bets heavily on their cloud offerings, with multiple clients and devices, not necessarily on premise or within their suite of products. In this article, I would like to highlight some of the key takeaways I have noticed during this years conference.

Cloud, cloud, cloud

Did I already said cloud? So the main message here is that Microsoft is dedicated one hundred percent to the cloud and their Office 365 offerings. No longer will Office 365 be trailing on the on-premise, full server product. Instead, all features will be implemented online first, in a 90 days update cycle, and be offered to the server versions in later updates. The Windows Azure platform is maturing, although obvisously there are still limitations to what you can do opposed to the on-premise freedom. This freedom however will less outweigh the costs more and more.

Search

Microsoft acquisition of Fast Server has lead¬†to the full integration of the product in 2013. No longer will SharePoint 2013 offer two search engines, but just one. Microsoft has worked the capabilities of the product to be flexible, yet easy to use. Where Search was primarily used for serving search results to the end users, Search will be more and more used for targeted publishing and content disclosure. Search driven applications will find their way to the desktop (Windows 8), the tablet (Microsoft Surface) and phones (Windows Phone 8). Apps are the keyword here and will release a new way of interacting with SharePoint, with rich UI’s of which the end users is unaware of SharePoint. Really cool stuff. Daniel Kogan showed a very interesting session on what you can do with search.

Social

Yeah, so are we getting tired of Social already. Well, social features are taken to the next level with SP 2013. The My Site concept will be replaced with SkyDrive Pro, where people will find their newsfeed on their home site. This newsfeed will surface documents they are following, people, people that follow them, discussions, @mentions and the works. With Yammer tob e fully integrated, Social will become a more vital part of SharePoint than ever before. Key thing here is that anything can be tracked. People, Sites, Documents, Discussion, Tags, @Mentions and so on. And as each of those entitites can have other entities linked, endless possibilities arise when working with Social objects. Imagine what we could do with Apps running on both Windows 8, Windows 8 RT (Surface) and the Phones.

Embracing open standards

Yes, Microsoft has embraced open standards, to the full! This means that they are fully targeting on HTLM5, JavaScript and for example REST services. Any major browser on any platform is now supported to interact with SharePoint, removing the boundaries within companies and end-users on how to consume the content. Output from SP in term of HTML has again undergone a serious overhaul, removing all non-neccessary HTML and complex structures. No ActiveX, but JavaScript. The standard api webservices are deemed depricated in this version and replaced with REST services, enabling communication and interaction across multiple platforms.

OAuth

What? Yeah, copy that. Microsoft has introduced a new security mechanism based on the OAuth¬†protocol to leverage interfarm¬†and inter product communication. This was neccessary¬†to fully integrate products like Exchange, Lync¬†and SQL Server. The OAuth¬†principles are based on the Kerberos priciples, with tokes and claims. They use a Trust Broker to mediate between two products (like for example SP and Exchange) and serve secure communication. It will be a challenge for developers to fully grasp this concept and make more flexible architectures between LoB’s and SharePoint.

Apps!

Following the trend, SharePoint now also embraces the concept of Apps. In fact, anything within SharePoint, whether it is a document library, list or site, is now considered an App. It bridges the gap between consumers and SharePoint on how to work and interact with SharePoint. Apps can be published and shared through the Office Store and SharePoint Store, which can also contain apps from thrid parties, like Bamboo for example. A whole variety of apps can be created through Visual Studio 2012, written in various languages, to extend and enhace the out of the box features.

Customizing the look and feel

SharePoint 2013 features a new Themes engine that can seriously change the way your SharePoint site looks and feels. Easier than ever before, the clean up of HTML and overhaul of the SharePoint style sheets will make it far more easier to created branded sites in SharePoint. The CSS classes have been renamed to more semantical¬†names like BodyText, BodyTitle¬†and so on, so that developers understand which part of the UI they are customizing. The number of CSS files have been reduced and are easier to navigate. New files like spcolor¬†and spfonts¬†allow for easier seggregation¬†between master pages and css files and will speed up your modification of your site. SharePoint 2013 also comes out of the box with a variety of Themes that also includes backgrounds, logo’s and even allow pictures to assume the colors of the theme. Really cool!

Well, I could go on for ages on what has changed in SharePoint 2013, as well as covering all the new features. In fact, so much has changed in 2013, that i twill require quite some investments from the community to get up to speed and discover the possibilities of what SharePoint 2013 offers. Exiting times, where I see the full ecosystem and Microsoft strategic decisions coming together. During the conference, I picked up a MS Surface tablet and really, once you have experienced that, you notice how limited and iPad was. A marvelous device, I love it.

So go out and experience SharePoint 2013. Be insipred!

Windows Surface Tablet ‚Äď Windows 8 rules

Well, it has been a long time since I wrote a blog post. Various reasons, but most of all just work. We work a lot ūüėČ

That said, exciting times coming up with Wave 15 just around the corner. But MS announced something else today, the new Windows Surface Tablet!

Sure, many will say, too little, too late, too much haging between two worlds, but I must say, after some real life demonstrations on comparable devices (Samsungs primarily) I am convinced.

It now truly becomes a full replacement for the laptop that combines all the freedom, but also offers the easy of use of Tablets these days.
With common stuff like an App Store (but then better with area’s you can make specific for your company), thin layout, capabilities to connect stuff like mouse, monitor, usb support, good Wifi, integration in office networks, side loading, pen support, good graphics, lightweight, your own software, touch or type keyboard attached, stand to avoid a docking station and obviously touch support, it almost sounds to good to be true.

Several colleagues have already replaced their laptop with a Samsung based Win 8 device and never want to go back. And these guys run Visual Studio on their tablet!

So view this announcement video and be impressed! Give me one when it is out!

http://cdn-smooth.ms-studiosmedia.com/news/mp4_mq/06182012_Surface_750k.mp4

Custom Email activity for SharePoint Designer 2010

You have probably run into it if you created SharePoint 2010 workflows using SharePoint Designer. The cool part of SharePoint 2010 is that it actually allows you to modify or copy the out-of-the-box workflows. The not so cool part is that the activities in SharePoint Designer are quite limited in their functionality.

For example, in the email activity that comes out of the box it is not possible to add attachments, nor does it allow you specify the From field.

Fortunately, using Visual Studio, we can upgrade the toolbox for SharePoint Designer activities to add more specialized activities that can then be used in the designer workflows.

Sure, it is possible to just create the workflow in Visual Studio, but to me, the hassle of creating a complete Visual Studio workflow, just because you want to change the From field does not make sense. It is not cost effective. SharePoint Designer workflows are powerful and should be part of the evaluation for any workflow solution. It has its limits, but also a lot of strengths.

In this post, I will show how to create a custom workflow activity for this purpose. To keep things simple, if possible using this subject, I will only allow for a single attachment. I will leave it up to you to update the activity with multiple attachments if you want to. Before I begin though, if you would like to just buy a lot of additional custom activities for SharePoint 2010, please take a look at the offerings from Nintex.

To build this feature, we will follow the following steps:

  1. Set up a standard SharePoint solution in Visual Studio 2010
  2. Define our ACTIONS file, which tell SharePoint and SharePoint Designer which activities I would like to include
  3. Define our SendMail class, which will implement the actions specified in the ACTIONS file
  4. Register our new custom action in the web.config using an eventreceiver (I will post a cool feature to do this easily in a later post)
  5. Package, deploy and test

Set up the solution structure

First, create a new Visual Studio solution using the SharePoint empty project template . Ensure that you have a key, so the resulting assembly is signed and deployable to the GAC. Then add a SharePoint Mapped folder, pointing to the following location:

Template\<yourlanguageLCID>\Workflow

Once done, add a file in this folder with the extension ACTIONS. You can call the file any way you want, but I used BoomCustomActivities.ACTIONS. Your solution should look like this now:


Define the ACTIONS file for our activity

We will now define which actions we will add to the toolbox. These will become available in the Actions tab on the Ribbon in SharePoint Designer. Populate the ACTIONS file with the following XML definition:

<?xml version=1.0encoding=utf-8?>
  <WorkflowInfo Language=en-us>
    <Actions Sequential=thenParallel=and>
<Action
Name=Send email with attachment
ClassName=$SharePoint.Project.FileNameWithoutExtension$.SendEmailActivity
Assembly=$SharePoint.Project.AssemblyFullName$
AppliesTo=all
Category=Email actions>
<RuleDesigner
Sentence=Send email with attachment %1 to %2. Use %3 as the sender>
<FieldBind Field=AttachmentFileNameText=this file (url)DesignerType=TextAreaId=1/>
<FieldBind Field=To,CC,Subject,BodyText=these user(s)DesignerType=EmailId=2/>
<FieldBind Field=FromText=this userDesignerType=TextAreaId=3/>
</RuleDesigner>
<Parameters>
<Parameter Name=__ContextType=Microsoft.SharePoint.WorkflowActions.WorkflowContextDirection=In/>
<Parameter Name=__ListIdType=System.String, mscorlibDirection=In/>
<Parameter Name=__ListItemType=System.Int32, mscorlibDirection=In/>
<Parameter Name=__ActivationPropertiesType=Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties, Microsoft.SharePointDirection=Out/>
<Parameter Name=AttachmentFileNameType=System.String, mscorlibDirection=In/>
<Parameter Name=ToType=System.Collections.ArrayList, mscorlibDirection=In/>
<Parameter Name=CCType=System.Collections.ArrayList, mscorlibDirection=Optional/>
<Parameter Name=SubjectType=System.String, mscorlibDirection=In/>
<Parameter Name=Body” Type=System.String, mscorlibDirection=Optional/>
<Parameter Name=FromType=System.String, mscorlibDirection=In/>
</Parameters>
</Action>
</Actions>
</WorkflowInfo>

Let’s discuss the contents. The WorkflowInfo node just indications this definition applies to Workflows. The Actions node defines how the text is build up in the workflow designer. For example, if used in a sequential action, the action will read <actiondescription> then. Nothing really interesting there. The real magic starts with the Action node. In there, the attributes Name, ClassName, Assembly, AppliesTo and Category define the name of the action in the actions menu, the class and assembly for the code, if it relates to list items, documents only or all, and the Category in which it is listed in the actions menu.

The RuleDesigner node specifies the text shown in the editor and related input parameters. The attribute Sentence specifies the sentence shown in the designer. You specify each parameter by a % followed by a number. %1 will point to the first parameter and so on. The parameters are specified using FieldBind attributes. The Text attribute substitutes the ‘%’ parameter indicator in the sentence in the designer. The Id attribute should match the order of the parameter.

The Parameters node and subsections define the parameters that will be passed into your custom activity class. Parameters obtained from the FieldBinding should exactly match the parameter names in the parameters section for them to be transferred to your code. You also specify whether your parameter is in or out and the type of the property, which is a .NET type. For more information on Workflow Action files, see MSDN.

So now we have our ACTIONS definition file. Let’s move on to the code itself.

Create the class for our activity

Add a new class to your project. Call the class SendEmailActivity as stated in the ACTIONS file. Have the class inherit from the System.Workflow.ComponentModel.Activity class. That is the easy part. Now for each of the parameters we stated in the ACTIONS file, we need to create properties and ensure that they map. First do the Workflow Context properties. They use a DepencyProperty to map the property in the class to the parameter stated in the ACTIONS file. See below code snippet.

#region [ Workflow Context Properties ]

public static DependencyProperty __ContextProperty = DependencyProperty.Register(“__Context”, typeof(WorkflowContext), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Required)]
public WorkflowContext __Context
{
get
{
return
((WorkflowContext)(base.GetValue(__ContextProperty)));
}
set
{
    base.SetValue(__ContextProperty, value);
}
}

public
static DependencyProperty __ListIdProperty = DependencyProperty.Register(“__ListId”, typeof(string), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Required)]
public string __ListId
{
  get
{
      return ((string)(base.GetValue(__ListIdProperty)));
}

set
{
      base.SetValue(__ListIdProperty, value);
}
}

public static DependencyProperty __ListItemProperty = DependencyProperty.Register(“__ListItem”, typeof(int), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Required)]
public int __ListItem
{
  get
  {
    return ((int)(base.GetValue(__ListItemProperty)));
}

set
{
base.SetValue(__ListItemProperty, value);
}
}

public
static DependencyProperty __ActivationPropertiesProperty = DependencyProperty.Register(“__ActivationProperties”, typeof(SPWorkflowActivationProperties), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Required)]
public SPWorkflowActivationProperties __ActivationProperties
{
  get
{
return (SPWorkflowActivationProperties)base.GetValue(__ActivationPropertiesProperty);
}

set
{
base.SetValue(__ActivationPropertiesProperty, value);
}
}

#endregion
[ Workflow Context Properties ]

What is important here is that the name of the DependencyProperty is equal to the name of the property in the class, appended with Property. Also, the name of the property should be equal to the name of the parameter in the ACTIONS file. Now that we have these properties, we also map the rest of the properties that are more relevant to our solution.

#region [ Custom Workflow Properties ]

public static DependencyProperty ToProperty = DependencyProperty.Register(“To”, typeof(ArrayList), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Required)]
public ArrayList To
{
  get
{
return ((ArrayList)(base.GetValue(SendEmailActivity.ToProperty)));
}

set
{
base.SetValue(SendEmailActivity.ToProperty, value);
}
}

public
static DependencyProperty CCProperty = DependencyProperty.Register(“CC”, typeof(ArrayList), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Optional)]
public ArrayList CC
{
  get
  {
return ((ArrayList)(base.GetValue(SendEmailActivity.CCProperty)));
}

set
{
base.SetValue(SendEmailActivity.CCProperty, value);
}
}

public
static DependencyProperty SubjectProperty = DependencyProperty.Register(“Subject”, typeof(string), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Required)]
public string Subject
{
  get
{
return ((string)(base.GetValue(SendEmailActivity.SubjectProperty)));
  }

set
  {
    base.SetValue(SendEmailActivity.SubjectProperty, value);
  }
}

public
static DependencyProperty BodyProperty = DependencyProperty.Register(“Body”, typeof(string), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Optional)]

public
string Body
{
  get
  {
return ((string)(base.GetValue(SendEmailActivity.BodyProperty)));
  }

set

  {
base.SetValue(SendEmailActivity.BodyProperty, value);
  }
}

public static DependencyProperty AttachmentFileNameProperty = DependencyProperty.Register(“AttachmentFileName”, typeof(string), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Required)]
public string AttachmentFileName
{
  get
  {
    return ((string)(base.GetValue(AttachmentFileNameProperty)));
  }

set
  {
base.SetValue(AttachmentFileNameProperty, value);
  }
}

public static DependencyProperty FromProperty = DependencyProperty.Register(“From”, typeof(string), typeof(SendEmailActivity));
[ValidationOption(ValidationOption.Required)]
public string From
{
  get
{
return ((string)(base.GetValue(SendEmailActivity.FromProperty)));
  }

set
  {
base.SetValue(SendEmailActivity.FromProperty, value);
}
}

#endregion [ Custom Workflow Properties ]

So basically it is more of the same. It connects the parameters from the Action file to our code behind class. So now that we have our properties and our class is fed with the parameters entered by the workflow designer in SharePoint Designer, we can further construct our class to do something useful. The base class System.Workflow.ComponentModel.Activity contains a method called Execute, which is the entry point for our custom activity. To implement, override the method in your custom activity. See the implementation of the method below:

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
  using (var web = __Context.Web)
{
  try
{
       // Get all of the information we currently have about 
       // the item that this workflow is running on
var message = BuildMailMessage(web);
// get the attachment if specified
SPFile attachment = null;

try
{
using (SPSite site = new SPSite(AttachmentFileName))
{
using (SPWeb fileWeb = site.OpenWeb())
{
attachment = fileWeb.GetFile(AttachmentFileName);
string name = attachment.Name;
Stream ms = attachment.OpenBinaryStream();
message.Attachments.Add(new Attachment(ms, name));
}
}
}
catch (Exception ex)
      {
// log could not find file
Common.WriteFailToHistoryLog(web, WorkflowInstanceId, string.Format(CultureInfo.InvariantCulture, “Unable to add attachment. Could not find or load file ‘{0}’.”, AttachmentFileName));
}

      if (!string.IsNullOrEmpty(From))
{
message.From = GetMailAddress(__Context.Web, From);
}

if (message.To.Count > 0)
      {
        // Send email w/ attachments
SmtpClient smtpClient = LoadSmtpInformation();
smtpClient.Send(message);

// Log entry to Workflow History Log
Common.WriteSuccessToHistoryLog(web, WorkflowInstanceId, string.Format(CultureInfo.InvariantCulture, “Email successfully sent to the following recipients: {0}”, message.To));
      }
      else
      {
// Log entry to Workflow History Log
StringBuilder emailAddressesTo = new StringBuilder();

for (int i = 0; i < To.Count; i++)
        {
¬†¬†¬†¬†¬†¬†¬†¬†¬† emailAddressesTo.AppendFormat(CultureInfo.InvariantCulture, “{0}, “, To[i]);
}

        // Trim off last comma
emailAddressesTo = emailAddressesTo.Remove(emailAddressesTo.Length – 1, 2);
Common.WriteFailToHistoryLog(web, WorkflowInstanceId, string.Format(CultureInfo.InvariantCulture, “Unable to send email out. No valid user email addresses for the following users ({0}) were found.”, emailAddressesTo));
}
   }
   catch (Exception ex)
   {
// Log entry to Workflow History Log
Common.WriteFailToHistoryLog(web, WorkflowInstanceId, string.Format(CultureInfo.InvariantCulture, “Failed to send email. Error message: {0}”, ex.Message));
   }
   finally
   {
      // Cleanup РDispose of these private properties before exiting
if (__ActivationProperties != null)
{
__ActivationProperties.Dispose();
}
if (__Context != null)
{
__Context.Dispose();
}
}
  }

return ActivityExecutionStatus.Closed;
}

To walk through the method, we first get the current web through the Workflow Context. We then use a couple of helper methods to build the mail message (BuildMailMessage), get the SMTP information from SharePoint (LoadSmtpInformation) and get the mail address of a user if it is passed as an account name (GetMailAddress). Please review those methods yourself if needed. Please note the LoadSmtpInformation method. It requests the outbound server address from the WebApplication. Be aware that multiple SMTP settings exist in SharePoint 2010 and that you can specify the outbound server both on server level in Central Admin and for each web application separately. In our case, we use the latter.


private static SmtpClient LoadSmtpInformation()
{
  string smtpServer = SPAdministrationWebApplication.Local.OutboundMailServiceInstance.Server.Address;
return new SmtpClient(smtpServer);
}

After we created the message, we try to load the attachment specified by the property. To do that, we use the AttachmentFileName property to open the SPSite where the file resides. We then open the SPWeb and use the GetFile method to load the file into a SPFile object. Finally, we use a MemoryStream to add the attachment to the mail message.

The rest of the method is to assign the From and To mail addresses. You can further expand the activity to also include CC and BCC addresses if you wish.

Register your custom action

Before you can use your custom action, you need to register it with SharePoint in the web.config. We therefore use a feature receiver to do this when activated. Add a feature to your solution and attach a feature receiver. Implement the receiver as follows:


public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
  SPWebApplication webapp = (SPWebApplication)properties.Feature.Parent;
  UpdateWebConfig(webapp, true);
}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
  SPWebApplication webapp = (SPWebApplication)properties.Feature.Parent;
UpdateWebConfig(webapp, false);
}

private void UpdateWebConfig(SPWebApplication webApp, bool featureActivated)
{
¬† SPWebConfigModification modification = new SPWebConfigModification(“authorizedType[@Assembly=\”Boom.WorkflowActivities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1f97cfd14d08de08\”][@Namespace=\”Boom.WorkflowActivities\”][@TypeName=\”*\”][@Authorized=\”True\”]”, “configuration/System.Workflow.ComponentModel.WorkflowCompiler/authorizedTypes”);

¬† modification.Owner = “Boom.WorkflowActivities”;
  modification.Sequence = 0;
  modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
¬† modification.Value = string.Format(CultureInfo.InvariantCulture, “<authorizedType Assembly=\”{0}\” Namespace=\”{1}\” TypeName=\”{2}\” Authorized=\”{3}\”/>”, new object[] { “Boom.WorkflowActivities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1f97cfd14d08de08”, “Boom.WorkflowActivities”, “*”, “True” });

  if
(featureActivated)
webApp.WebConfigModifications.Add(modification);
  else
    webApp.WebConfigModifications.Remove(modification);

SPFarm
.Local.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
}

This will add or remove an authorizedType node to your web.config file, enabling SharePoint to use the action in SharePoint Designer workflows.

Add safecontrol entries to the package manifest

To ensure that SharePoint will safely load our custom activity, we add a SafeControl entry to the web.config. We do that by overriding the Package.Template.xml to include our SafeControl entry. Also note we use the VS ability to inject the assembly details on build.

<Assemblies>
  <Assembly Location=Boom.WorkflowActivities.dll DeploymentTarget=GlobalAssemblyCache>
<SafeControls>
<SafeControl Assembly=$SharePoint.Project.AssemblyFullName$ Namespace=$SharePoint.Project.FileNameWithoutExtension$ TypeName=* />
</SafeControls>
</Assembly>
</Assemblies>

Build and Test

So, the hard work is now done. The last thing to do is have VS build the package and deploy to your environment. Once an IIS reset (or app pool recycle) has been done, fire up SharePoint Designer and connect to your site. Create a new SharePoint Reusable Workflow and see if your custom action is there:

Figure 1: Custom action available

Figure 2: Complete the action parameters

Conclusion

You can do a lot with the reusable workflow abilities in SharePoint Designer 2010. However, the best part is that you can extend the capabilities that the OOB version has, which makes the SharePoint workflows far more likely to be used as they can be tweaked. A definite improvement over SharePoint 2007 where they were as they came.

Have fun building your own!

SharePoint's Booming world

%d bloggers like this: