Tuesday, July 2, 2013

Upgrade custom workflow in SharePoint



Experience comes when you give a try or do something, I worked in to many SharePoint development project but my last project was troubled me during the deployment on staging server. I really interested to share the learning with all of you.

Recently we got an enhancement in one of our existing SharePoint project that has custom solution. The custom solution package contains workflows, application pages and InfoPath form. The site content was huge (25 GB), we got the site collection backup and restored on development environment and removed most of the contents to make more space on our development server. At the same time we got the requirement (enhancements) 1. Changes on one InfoPath form (adding additional fields) and one custom workflow (update in the workflow business logic and activities).

I have started the design of all enhancements but end of with major problem on the workflow. SharePoint does most of the things as per customer expectation but gives some pain to the developers and designers. Normally when we update and publish the SharePoint designer workflow, it creates a new version allow new instance on that new version and automatically not allows previous instances for new item. But this is a major issue in custom workflow. When user try to complete the existing workflow instances after updating the assembly, they may get "Task is locked by running workflow instance" error or it just hangs in "In Progress" state.. It is a very bad experience. I had read many articles which points the same issue.

If developer updates on the same DLL there are problems for existing items and there no way you can allow old workflow instances.  See the expected problems below.

  1. The old workflow DLL will be overwritten by new DLL in the GAC of deployment server
  2. If any changes done on the workflow activities then all “In progress” (old instances) workflow items cannot be completed.
  3. If changes done only on the business login then better to complete all “In Progress” workflow items before new DLL deployment.

I found a good article which explains above problem clearly, find some content and relates links below.



The following sections discuss three ways in which you can upgrade and version a workflow.

Option 1: Move all content to new site and re-execute incomplete workflows

You can create a new SharePoint site, register the new version of workflow on that site, and migrate list or document items that have workflows associated with them, omitting completed workflows. For items that have workflows pending, re-execute those workflows from scratch.

This option isn't great, because it requires data migration between the old SharePoint sites to the new SharePoint site. If your workflow fixes also modify the format of the underlying site (such as new fields in lists), the migration is nontrivial. Also, you have to re-execute your workflows that were not done, which adds burden on users who have already done their part to move workflow along.

Option 2: Code workflow so it can continue or restart all in-process workflows

For state machine workflows, you have an option of designing your state transition flow to accommodate possible future restarts. If your workflow process keeps some kind of state information in the list item or document properties, that state information is most likely a field in the list or document library. Your workflow probably expects a certain initial state value when it starts. A workflow that is already running would naturally have a different in-process state value.

If you code your workflow to accommodate any state value (not just the initial one) and during start-up jump to the code that executes that state, you can remove all completed and running workflows, remove the old workflow association with the old workflow assembly version, add a new workflow association with a new workflow version, and then re-execute all pending workflows.

Naturally, this approach puts a burden on you as a developer to add numerous conditional "re-entry/re-route" points to the beginning of your workflow. You may not want to do this extra work, or you may not be able to thoroughly test all re-entry scenarios. Also, this design is feasible for state machine workflows only.

Option 3: Run old workflows to completion with old assembly and new workflows with new assembly

You can change the mode of a workflow association to "No New Workflows," which stops new workflows from starting but allows existing ones to complete. By putting old workflow associations into "No New Workflows" mode and adding new workflow associations, you can have multiple versions of a workflow assembly running at the same time.

For example, you can have a workflow association named "MyCustomWorkflow v1.0.0.0" that is associated with version 1.0.0.0 of your workflow assembly in the "No New Workflows" mode. You can then add "MyCustomWorkflow v1.0.1.0" associated with 1.0.1.0 version of your workflow.

This is not the ideal solution either. For example, if the old workflow assembly has a bug that is not fixed, continuing to run the old workflow with the old assembly isn't helping you. Also, if the "On Item Change" event is enabled for the new workflow association, every time the item with the old workflow changes, it starts an instance of the new workflow. However, in some scenarios this approach remains the most valid.

Now, you need to deploy the new workflow assembly without removing the old version. Although stsadm.exe has the "upgradesolution" command, all it does is replace the old solution package with the new one. When the old solution package is removed, old workflow assembly will be removed from GAC as well.

To deploy a new workflow package over an old one

  1. Change the workflow assembly version.
  2. Change the workflow assembly version in the CodeBesideAssembly attribute of the Workflow element in Workflow.xml.
  3. Change the solution GUID in the manifest.xml file of your workflow project.
  4. Change the name of the .wsp solution package that is generated by package.ddfor rename the generated .wsp file.
  5. Deploy the new solution package alongside the old solution package, thus registering a new version of the workflow assembly in the GAC.
  6. Perform an IISReset command.
  7. Put the old workflow association into "No New Workflows" mode.
  8. Manually or programmatically create the new workflow association alongside the old workflow association, being sure supply a different name.

 

Tuesday, September 18, 2012

Categorize discussion Forum threads in SharePoint Or Create folder in discussion forum


Creating folders feature is not available in discussion forum list using OOTB. But this can be achieved using custom coding but quite complex. Let us see the workaround for same in OOTB. Actually the discussion forum is uses two different content types (discussion and message). The discussion content type is derived from Folder content type and the message content type is derived from Item content type.  The folder content type is represented in different way through Discussion of course you cannot see the folder image but it works as folder. So the discussion forum simply not allows user to create folders, but the discussion threads and its replies can be categorized using  some trick.
As I said before when there are two content type are used in discussion forum and the mapping is done internally in discussion forum template. If user creates a new discussion thread, by default it uses “Discussion” Content type thereafter it starts using the “Message” content type. To categorize the discussion threads try below trick.

1.       Create a discussion forum list.
2.       Add an additional column “Category”

3.       Navigate to List setting of discussion forum list
4.       Open the Discussion content type and make sure the newly created “Category” fields is available.
 
5.       Open the Message content type and click on the “Category” field.

6.       Click on the Remove button to delete the field from Message content type.


7.       Make sure that field “Category” is available only in Discussion” content type.
8.       Create new threads and pass category value.
9.       Add a reply to the existing thread and make sure that the category filed is not shown.

10.  Modify the default view and apply group by on Category field.
11.  Navigate the discussion forum home page and check is that what is expected.


Enjoy learning SharePoint :-)

Saturday, August 25, 2012

Ensure User SharePoint 2010 using web service


SharePoint is a product from Microsoft that is being used in many of the organization today to improve the business efficiency. The product provides capability to business like Sites, Communities, Content, Composites, Insights and Search.
Also it offer three different ways of accessing the SharePoint data as follows
1.       SharePoint site web user interface
2.       Server Object Model
3.       Client Object Model
The security is well handled in SharePoint itself. If user accesses the data using any of the way that mentioned above, first it applies the security rule then SharePoint will decide whether the request can be processed further or not. But there are some security rule applied against Client Object Model since this concept can be used from a client computer where the SharePoint is not installed or via web service. See all the web services here.
Now we discuss about Ensure User: Checks whether the specified logon name belongs to a valid user of the website, and if the logon name does not already exist, adds it to the website.  Server and Client object models directly exposes a method (Web.EnsureUser) to add or verify the user in the Website. Also this is to let you all know that a user (AD DS/LDAP/Membership Provider) can be added in the SharePoint userinfo list which is hidden using SharePoint web service(Web service reference: http://Site/_vti_bin/People.asmx).

People Web Service: Provides classes that can be used to associate user identifiers (IDs) with security groups for SharePoint Foundation Web site permissions. User IDs are validated against Active Directory Domain Services (AD°DS) as well as various role or membership providers. SPGroup security information may also be stored in a collection of cross-site groups for the site collection.
Class: People  Structures: PrincipalInfo Enumerations: SPPrincipalType
Class People that exposes member are here

Method People.ResolvePrincipals is used for adding user in to SharePoint userinfo list. User verification is performed against a directory or user list, such as Active Directory Domain Services (AD DS), a Lightweight Directory Access Protocol (LDAP) directory, some other role or membership providers, or another form of user list.

Parameters

principalKeys
Type: System.String[]
Logon name of the principal.
principalType
Type: [People Web service].SPPrincipalType
SPPrincipalType object that specifies user scope and other information.
addToUserInfoList
Type: System.Boolean
Indicates whether to add the principal to a SPUserCollection that is associated with the Web site.

Return Value

Type: [People Web service].PrincipalInfo[]
A list of PrincipalInfo objects that is indexed and accessed by the logon name in the AccountName() field.


        static string WS_EnsureUser(string domain, string loginName)
        {
            string[] userLogin = new string[] { domain + "\\" + loginName };
            string userInfo = string.Empty;
            try
            {
                WSPeople.People ppl = new WSPeople.People();
                ppl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
                ppl.Url = "http://kmsnet:5050/_vti_bin/people.asmx";

                //Get user info if already added in the site
                WSPeople.PrincipalInfo[] principalInfoUser = ppl.ResolvePrincipals(userLogin, WSPeople.SPPrincipalType.User, false);
                if (principalInfoUser[0].UserInfoID == (-1))
                {
                    //Add and Get user info if user is not added in the site
                    principalInfoUser = ppl.ResolvePrincipals(userLogin, WSPeople.SPPrincipalType.User, true);
                }
                //get SharePoint user id(unique) and user account name
                userInfo = principalInfoUser[0].UserInfoID + ";#" + principalInfoUser[0].AccountName;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error in Web Service  : " + ex.Message);
            }

            return userInfo;
        }


Complete Source Code:


using System;
//Server Object Model
using Microsoft.SharePoint;
//Client Object Model
using Microsoft.SharePoint.Client;

namespace KMSNET.EnsureUser
{
    class Program
    {
        static void Main(string[] args)
        {
            string domain = "KMSNET";
            string loginName = "Senthilrajan";
            string userInfo = string.Empty;

            //Call web service to Ensure user in SharePoint site
            userInfo = WS_EnsureUser(domain, loginName);
            Console.WriteLine("\nWeb Service Returned :" + userInfo);

            //Call Client Object Model to Ensure user in SharePoint site
            userInfo = COB_EnsureUser(domain, loginName);
            Console.WriteLine("\nClient Object Model Returned :" + userInfo);

            //Call Server Object Model to Ensure user in SharePoint site
            userInfo = SOB_EnsureUser(domain, loginName);
            Console.WriteLine("\nServer Object Model Returned :" + userInfo);

            Console.ReadKey(true);            Console.WriteLine("Press any key to close....");
        }

        /// <summary>
        /// Server Object Model : Returns user ID and logon account. If user is not registered the add in to userinfor list and return the user ID and logon account
        /// </summary>
        /// <param name="domain"></param>
        /// <param name="loginName"></param>
        /// <returns></returns>
        static string SOB_EnsureUser(string domain, string loginName)
        {
            string userLogin = (domain + "\\" + loginName);
            string userInfo = string.Empty;
            try
            {
                using (SPSite siteColl = new SPSite("http://kmsnet:5050/"))
                {
                    using (SPWeb rootWeb = siteColl.RootWeb)
                    {
                        // Allow unsafe updates required, throws exception without, if not administrator.
                        rootWeb.AllowUnsafeUpdates = true;

                        SPUser usr = rootWeb.EnsureUser(loginName);
                        userInfo = usr.ID + ";#" + usr.LoginName;

                        rootWeb.AllowUnsafeUpdates = false;
                    }
                }

            }
            catch (Exception ex)
            {
                Console.WriteLine("Error in Server Object Model  : " + ex.Message);
            }
            return userInfo;

        }

        /// <summary>
        /// Client Object Model : Returns user ID and logon account. If user is not registered the add in to userinfor list and return the user ID and logon account
        /// </summary>
        /// <param name="domain"></param>
        /// <param name="loginName"></param>
        /// <returns></returns>
        static string COB_EnsureUser(string domain, string loginName)
        {
            string userLogin = (domain + "\\" + loginName);
            string userInfo = string.Empty;
            try
            {
                using (ClientContext context = new ClientContext("http://kmsnet:5050/"))
                {
                    context.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
                    Web web = context.Web;

                    User usr = web.EnsureUser(userLogin);
                    context.Load(usr);
                    context.ExecuteQuery();
                    userInfo = usr.Id + ";#" + usr.LoginName;
                    context.ExecuteQuery();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error in Client Object Model  : " + ex.Message);
            }
            return userInfo;
        }

        /// <summary>
        /// Web Service : Returns user ID and logon account. If user is not registered the add in to userinfor list and return the user ID and logon account
        /// </summary>
        /// <param name="domain"></param>
        /// <param name="loginName"></param>
        /// <returns></returns>
        static string WS_EnsureUser(string domain, string loginName)
        {
            string[] userLogin = new string[] { domain + "\\" + loginName };
            string userInfo = string.Empty;
            try
            {
                WSPeople.People ppl = new WSPeople.People();
                ppl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
                ppl.Url = "http://kmsnet:5050/vti_bin/People.asmx";

                //Get user info if already added in the site
                WSPeople.PrincipalInfo[] principalInfoUser = ppl.ResolvePrincipals(userLogin, WSPeople.SPPrincipalType.User, false);
                if (principalInfoUser[0].UserInfoID == (-1))
                {
                    //Add and Get user info if user is not added in the site
                    principalInfoUser = ppl.ResolvePrincipals(userLogin, WSPeople.SPPrincipalType.User, true);
                }
                //get SharePoint user id(unique) and user account name
                userInfo = principalInfoUser[0].UserInfoID + ";#" + principalInfoUser[0].AccountName;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error in Web Service  : " + ex.Message);
            }

            return userInfo;
        }
    }
}