Friday, April 13, 2007

Multi Value Columns Solution #2 - Custom Activities in SPD

In the previous post in this series (Multi-Value Columns in SharePoint Designer - Solution #1), I described a problem where SharePoint Designer can’t send e-mail to multiple recipients if those recipients exist inside of a multi-value column in a SharePoint list. The simple hacky solution I described was to temporarily turn the column into a single value column just for SharePoint Designer. But this approach has problems, and there is a better way: developing custom activities in Visual Studio for use in SharePoint Designer.

In this post I will describe how to develop a custom activity in Visual Studio that will also solve this problem and I will also describe how to install it on a SharePoint server so that SharePoint Designer clients can automatically download and use it. Before I do let me back up and tell you why, from my perspective, this approach is probably not the best way to go.

SharePoint Designer is a Microsoft Office product that replaces FrontPage, integrates tightly with SharePoint, and allows non-developers (aka “knowledge workers” in the Microsoft lingo) to create simple workflows without writing any code.

The problem is that simple workflows and multi-value columns are like oil and water: not so compatible. If you’re using multi-value columns then your knowledge workers should admit defeat and let developers create the workflows in Visual Studio using the Windows Workflow Foundation (which, incidentally, will be the topic for third article in this series).

Still, you may have a good reason for continuing to develop workflows in SharePoint Designer, and I’ve already gone through the pain of writing and installing custom activities, so hopefully this post will make life easier for someone somewhere.

Creating the Custom Activity in Visual Studio

The code to create the custom activity in Visual Studio is the most interesting part of this solution. Make sure to check out the GetEmailAddressesFromUsernames() method if you have time to review the code. Here is the procedure assuming this is your first time working with Windows Workflow Foundation in Visual Studio.

1. The first step is to download and install Visual Studio 2005 extensions for .NET Framework 3.0 (Windows Workflow Foundation) which will add workflow options to Visual Studio.

2. If you aren’t running on Windows Server 2003, then you probably need to install the Windows Workflow Foundation DLL’s. You’ll know there’s a problem if, after creating the project in Visual Studio, your SharePoint references are invalid. I picked these DLL’s up from my Windows Server 2003 machine (actually a VMWare virtual machine) from the following location:

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\ISAPI

You should install these to the Global Assembly Cache (GAC) to make Visual Studio happier, although you may still need to reference them for your project. The easy way to install to the GAC is just to copy them to somewhere on your local (non-W2K3) machine and then drag them all over to C:\Windows\assembly.

3. After completing step #1 when you open Visual Studio you should get a new tree node under “Visual C#” (or Visual Basic) called “Workflow.” Select that, then “Workflow Activity Library” then call the project something like “MultiRecipientMail”.

4. Visual Studio should automatically create a blank activity called Activity1.cs. If you want a more reasonable name then delete it and “Add” a “New Item” of type “Activity” called something like “MultiRecipientMailActivity.” Don’t make the same painful mistake as me: name your activity something other than the name of the project or anything in the namespace.

5. At this point you could drag and drop a “Replicator” activity on the design surface and put a “Send Mail” activity inside, but I’ll cover that in my next post. For now I think code is the clearer way to go. Hit F7 or right click and “View Code” and paste the following which I’ll try to comment in-line rather than in this article.

// Note: this code was adopted from Todd Baginski at http://www.sharepointblogs.com/tbaginski/archive/2007/03/08/HOW-TO_3A00_-Create-a-custom-Windows-Workflow-Activity-and-make-it-available-in-SharePoint-Designer.aspx

 

using System;

using System.ComponentModel;

using System.ComponentModel.Design;

using System.Collections;

using System.Drawing;

using System.Workflow.ComponentModel;

using System.Workflow.ComponentModel.Design;

using System.Workflow.ComponentModel.Compiler;

using System.Workflow.ComponentModel.Serialization;

using System.Workflow.Runtime;

using System.Workflow.Activities;

using System.Workflow.Activities.Rules;

using System.Net.Mail;

using Microsoft.SharePoint;

using System.IO;

 

namespace MultiRecipientMail {

    public partial class MultiRecipientMailActivity : SequenceActivity {

 

        // Windows Workflow Foundation (WWF) will store the workflow state using these "instance property"

        //      dependency properties for more info check out: http://msdn2.microsoft.com/en-us/library/ms733604.aspx

 

        // the ToProperty must be bound to a multi-value column of users otherwise the activity will fail

        public static DependencyProperty ToProperty = DependencyProperty.Register("To", typeof(string), typeof(MultiRecipientMailActivity));

        // the FromEmailAddress does not support multi-value columns, it must be static or bound to a single-value column

        public static DependencyProperty FromEmailAddressProperty = DependencyProperty.Register("FromEmailAddress", typeof(string), typeof(MultiRecipientMailActivity));

        public static DependencyProperty SubjectProperty = DependencyProperty.Register("Subject", typeof(string), typeof(MultiRecipientMailActivity));

        public static DependencyProperty BodyProperty = DependencyProperty.Register("Body", typeof(string), typeof(MultiRecipientMailActivity));

        public static DependencyProperty SMTPServerNameProperty = DependencyProperty.Register("SMTPServerName", typeof(string), typeof(MultiRecipientMailActivity));

 

        // the ValidationOptionAttribute is new to .Net 3.0 and is used exclusively by WWF

        [ValidationOption(ValidationOption.Required)]

        public string To {

            get {

                return (string)base.GetValue(ToProperty);

            }

            set {

                base.SetValue(ToProperty, value);

            }

        }

 

        [ValidationOption(ValidationOption.Required)]

        public string FromEmailAddress {

            get {

                return (string)base.GetValue(FromEmailAddressProperty);

            }

            set {

                base.SetValue(FromEmailAddressProperty, value);

            }

        }

 

        [ValidationOption(ValidationOption.Required)]

        public string Subject {

            get {

                return (string)base.GetValue(SubjectProperty);

            }

            set {

                base.SetValue(SubjectProperty, value);

            }

        }

 

        [ValidationOption(ValidationOption.Optional)]

        public string Body {

            get {

                return (string)base.GetValue(BodyProperty);

            }

            set {

                base.SetValue(BodyProperty, value);

            }

        }

 

        [ValidationOption(ValidationOption.Required)]

        public string SMTPServerName {

            get {

                return (string)base.GetValue(SMTPServerNameProperty);

            }

            set {

                base.SetValue(SMTPServerNameProperty, value);

            }

        }

 

        public MultiRecipientMailActivity() {

            InitializeComponent();

        }

 

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) {

            try {

                // log workflow started

                LogThis("MultiRecipientMail started");

 

                // create a MailMessage object to send the email

                MailMessage msg = new MailMessage();

 

                // set the from address

                MailAddress fromAddress = new MailAddress(this.FromEmailAddress);

                msg.From = fromAddress;

 

                // the To property is bound to a multi-value column of usernames (e.g. "Domain\username1; Domain\username2",

                //      so parse its contents out into an array of e-mail addresses

                string[] astrEmails = GetEmailAddressesFromUsernames(To);

 

                // loop through and add the To addresses to the MailMessage

                foreach (string strEmail in astrEmails) {

                    MailAddress toAddress = new MailAddress(strEmail);

                    msg.To.Add(toAddress);

                }

 

                // set the subject and body

                msg.IsBodyHtml = true;

                msg.Subject = Subject;

                msg.Body = Body;

 

                // create an SmtpClient object to represent the mail server that will send the message

                SmtpClient client = new SmtpClient(SMTPServerName);

                // how to authenticate to the email server

                client.UseDefaultCredentials = true;

                // send the message

                client.Send(msg);

 

                // log workflow started

                LogThis("MultiRecipientMail completed successfully");

            } catch (Exception ex) {

                LogThis("MultiRecipientMail error: " + ex.ToString());

            }

 

            // indicate the activity has closed

            return ActivityExecutionStatus.Closed;

        }

 

        private string[] GetEmailAddressesFromUsernames(string strUsernames) {

            // SharePoint stores multi-value columns to the format: "domain\user1; domain\user2" so split on ';'

            string[] astrUsernames = strUsernames.Split(';');

 

            // This beautiful piece of code I adapted from Finnatic at http://rfinn.spaces.live.com/Blog/cns!4E19D0E183DFC38B!204.entry

            //      I looked for a while to find an easier way to get e-mail addresses from user names, but no luck.  Perhaps web services.

            //      Anyway RunWithElevatedPrivileges is necessary to avoid permissions problems with UserProfileManager. It's argument is a

            //      delegate, so the anonymous delegate works.  This will probably fail if the user has no WorkEmail, but it's a starting point.

            //     

            SPSecurity.RunWithElevatedPrivileges(

                delegate() {

                    // connect to the root Sharepoint site

                    using (SPSite site = new SPSite("http://nic-lrichard-03:34089/")) {

                        Microsoft.Office.Server.ServerContext context = Microsoft.Office.Server.ServerContext.GetContext(site);

                        Microsoft.Office.Server.UserProfiles.UserProfileManager profilemanager = new Microsoft.Office.Server.UserProfiles.UserProfileManager(context);

 

                        // for each of the usernames in astrUsernames

                        for (int i = 0; i < astrUsernames.Length; i++) {

                            string strUsername = astrUsernames[i].Trim();

 

                            // convert the username to the WorkEmail address

                            Microsoft.Office.Server.UserProfiles.UserProfile profile = profilemanager.GetUserProfile(strUsername);

                            astrUsernames[i] = profile[Microsoft.Office.Server.UserProfiles.PropertyConstants.WorkEmail].Value.ToString();

                        }

                    }

                }

            );

 

            return astrUsernames;

        }

 

        /// <summary>

        /// This logging method is NOT production ready.  Obviously you need to create a C:\MultiRecipientMailLog.log

        /// with appropriate for debugging.  In production it should log to the event log or be removed.

        /// </summary>

        /// <param name="str"></param>

        private void LogThis(string str) {

            FileInfo fi = new FileInfo("C:\\MultiRecipientMailLog.log");

            if (fi.Exists) {

                StreamWriter sw = fi.AppendText();

                sw.WriteLine(String.Format("{0:g} {1}", DateTime.Now, str));

                sw.Flush();

                sw.Close();

            }

        }

   

    }

}

If you’re new to .Net 3.0 you’ll find the whole dependency property thing pretty confusing. I’ll write about it in another post. For now just think of it as a big weakly typed hash table whose values WWF can use without prior knowledge of their existence.

6. To get this to compile you’ll need to add a reference to “Microsoft.SharePoint” and “Microsoft.Office.Server.” If you’re on a non-W2K3 machine you may need to reference the DLL’s manually that you copied from step 2. Regardless, double check you can compile (Control+Shift+B or Build->Build MultiRecipientMail).

Deploying the Custom Activity to the SharePoint Server

To give credit up front, a large part of this procedure came from Todd Baginski’s Blog. However, I still had a lot of difficulties, and based on the comments on the Microsoft forums regarding the multi-value column problem, a refinement of Todd Baginski’s work from a different perspective will be valuable to the MOSS community. In any event everything regarding this multi-value column solution is now in one place.

7. You’ll need to strongly sign the project because it needs to go into the Global Assembly Cache (GAC). To do this:

    1. Right click on the project;
    2. Select properties;
    3. Select the signing tab;
    4. Check “Sign the assembly;”
    5. Select “New” from the dropdown;
    6. Enter a name like “MultiRecipientMail;”
    7. Uncheck “Protect my key file with a password;” and
    8. Click OK.

8. Compile (Control+Shift+B or Build->Build MultiRecipientMail)

9. Get the public key token which was generated as a result of 7 & 8:

1. Open the “Visual Studio 2005 Command Prompt,” from Start->Microsoft Visual Studio 2005->Visual Studio Tools;

2. Change directory to your output directory, which I usually copy and paste from Windows Explorer (e.g. cd “C:\Users\lrichard\Documents\Visual Studio 2005\Projects\MultiRecipientMail\MultiRecipientMail\bin\Debug”)

3. Type “sn -T MultiRecipientMail.dll” and copy and paste the public key token by right clicking and selecting mark then hitting Control-C. Save this token for later perhaps in notepad or commented out in your AssemblyInfo.cs file.

10. Now install your dll to the W2K3 GAC. To do this copy the dll to somewhere on the W2K3 machine (if it’s a separate machine), and either drag it to “c:\windows\assembly” or at a command prompt type something similar to:

gacutil.exe -uf MultiRecipientMail.dll (to uninstall if previously installed)

gacutil.exe -if MultiRecipientMail.dll (to install)

The latter is the correct way and works in a deployment script, but I’d opt for the former for convenience.

11. The next step is get SharePoint Designer to recognize the new action. You could modify the WSS.ACTIONS file as Todd Baginski suggests, but you’re better off leaving this core file alone and creating a new one in the same directory that SharePoint will automatically pick up upon reboot (ref). So inside of:

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\1033\Workflow\

Create a new text file called MultiRecipientMail.ACTIONS and paste in the following and save:

<?xml version="1.0" encoding="utf-8"?>
<WorkflowInfo Language="en-us">
    <Actions>
        <Action Name="Multi Recipient E-mail"
            ClassName="MultiRecipientMail.MultiRecipientMailActivity"
            Assembly="MultiRecipientMail, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d73739679587735e"
            AppliesTo="all"
            Category="Custom Actions">

            <RuleDesigner Sentence="Send %1, %2, %3, %4 via %5.">
                <FieldBind Field="To" Text="to" Id="1" DesignerType="TextArea"/>
                <FieldBind Field="FromEmailAddress" Text="from" Id="2" DesignerType="TextArea"/>
                <FieldBind Field="Subject" Text="Subject" Id="3" DesignerType="TextArea"/>
                <FieldBind Field="Body" Text="body" Id="4" DesignerType="TextArea"/>
                <FieldBind Field="SMTPServerName" Text="SMTP Server" Id="5" DesignerType="TextArea"/>
            </RuleDesigner>
            <Parameters>
                <Parameter Name="To" Type="System.String, mscorlib" Direction="In" />
                <Parameter Name="Subject" Type="System.String, mscorlib" Direction="In" />
                <Parameter Name="Body" Type="System.String, mscorlib" Direction="Optional" />
                <Parameter Name="FromEmailAddress" Type="System.String, mscorlib" Direction="In" />
                <Parameter Name="SMTPServerName" Type="System.String, mscorlib" Direction="In" InitialValue="127.0.0.1" />
            </Parameters>
        </Action>
    </Actions>
</WorkflowInfo>

You’ll need to substitute your own public key token from step 9 in the “Assembly=” statement.

12. Next you need to tell SharePoint designer clients to trust this new dll. To do so open the Web.Config in the virtual directory for your site (e.g. C:\Inetpub\wwwroot\wss\VirtualDirectories\34089\ web.config). Find the section called “” and add the following new line (substitute your own public token):

(again substitute your own public key token)

13. Restart IIS by opening a command prompt and typing “iisreset.” SharePoint will pick up the new MultiRecipientMail.ACTIONS file, find the dll in the GAC, and provide the new information to any SharePoint Designer clients that access the site.

14. Open SharePoint Designer and either navigate to the “Workflows\Multi Recipient Email.xoml” project from my previous article or create a new workflow project based on a list with a multi-value column. You should get a message like the following while SharePoint Designer downloads the new dll:

15. If everything worked correctly you should now be able to 1. click Actions; 2. More Actions; 3. Select the “Custom Actions” category from the dropdown (this category came from the .ACTIONS file in Step 11); and 4. Select the “Multi Recipient E-mail” action. If it doesn’t show up in the Workflow Designer page after clicking "add", then you probably referenced it incorrectly in the Web.Config.

16. Now you should be able to 1. click the Fx button next to “To;” 2. Select your multi value column (e.g. Peers To Review from my previous article); 3. Hit Ok; and 4. Fill in the remaining fields with values.

17. Now if you head back to Sharepoint, select the dropdown of a list item in the list associated with your workflow, click “Workflows”, select the workflow you created in SharePoint Designer, and click “Start” you should receive an e-mail at each address in your multi-value column.

Well … at least it worked for me. :) I did kitchen test this article, but please leave comments if it doesn't work for you.

Redeploying

If you want to make any changes to the code the steps to redeploy are:

1. Compile (you don’t need to sign again, that’s one time only)

2. Copy the dll to your W2K3 server

3. Re-install the dll to the GAC. You can just re-copy it over to c:\windows\assembly if you like.

4. Restart iis with an “iisreset” from the command line (no need to change the .ACTIONS or Web.Config files)

5. Finally, if you need to make changes to your workflow, you may need to restart SharePoint Designer to download the new dll (I usually have to).

You’re Done! Easy huh?

So you should now have a reusable component that non-developers can use in SharePoint Designer to send e-mail to multiple recipients as determined by a multi-value column in a SharePoint list. Of course if you decide that developing the workflow in Visual Studio is better, then the third article in this series may be for you.

12 comments:

Edgardo said...

Great Work! and thanks! I almost blew my head off because of this issue. I still have the problem of collecting data from a group (assigning a task to a group). Do you a workaround also?

Edgardo

Lee Richardson said...

Edgardo,

I'm so happy to hear I kept you from blowing your head off. I wish I had experience assigning a task to a group, but unfortunately I don't. Hopefully you don't feel as strongly about the latter issue as the former.

Best of luck,
- Lee

Fred said...

Im able to do al lthe steps, but in Sharepoint Designer and I add my custom activity, nothing happen, I dont see the RuleDesigner Sentence written. I have no FieldBind or parameter, its a really simple workflow that always do the same thing so I dont need any parameter passed, is that why it dont work?

David said...
This comment has been removed by the author.
David said...

Hi Lee,
Thanks for the post - very helpful! Between your post, Todd's, and Google I was able to get my custom activity working. I also found a slightly easier way of getting the email address from the user name by passing a WorkflowContext object as an input parameter and looking up the site or web's SiteUsers collection. I posted the info on my blog, as well as a complete example. Hope this helps! :-)
Cheers,
David

(I deleted my original comment and replaced with this one to fix the links)

Todd Baginski said...

Hi Lee,

I'm glad to see that my post was helpful to get you started on this topic and I think your post is excellent.

Todd

chris said...

can you explain your point no 12? is it missing something?Like the section name and the line to be added to the config file?

Anonymous said...

When I compile the code from step # 5, I get this error:

1. 'The name 'InitializeComponent' does not exist in the current context.
2. 'MultiRecipentMail.Activity1' does not contain a definition for 'Name'

How can I fix this?

Samar Hossam said...

Hi there,

I had the same compilation problem as you. Here is how I fixed it:

Assuming that your project name is "MyCustomActivity", then the namespace line should look like this:
namespace MyCustomActivity

If you would like to have a namespace like this:
namespace My.Custom.Activity
then you have to create a new project with exactly that name.

Hope this helps you.

Black Russian said...

This is really late but Edgardo Assigning a Form to a Group should work for you. it send tasks to each person in the group and waits for each

Anonymous said...

Great Article the example works fine. TQ

abhandari said...

Could you please clarify Step 12. What line should be added in .config file and where should it be added?


Thanks!!