Tuesday, June 25, 2013

Project Server - Get approver's username in workflow

In my previous posts, I mentioned many times the benefits of using Demand Management workflow for Project Server from Solution Starters project (I described it in this post).

I am very satisfied with its possibilities, but one flaw was really bothering me. Problem is, you can not fetch the username of approver of a stage in workflow. You can get to any sort of workflow data, but this approver information seems to be hidden.

But, after hours of searching,I managed to find it. I must say, that, this isn't the nicest solution, but I think it is the only one and this works 100 %. If you know any other way, please, let me know.

Approver's username is hidden in OfficeTask, in ConsolidatedComments. But, it is mixed with bunch of other stings, so, I extracted with this method:

//This is the method for reading Comments and extracting the username


private static string getApproversUsername()
{

    var officeTask = (OfficeTask)((Activity)sender).Parent.Parent.Parent;


    string[] _userComment = officeTask.ConsolidatedComments.Split(new string[] { "Comment:" }, StringSplitOptions.None);

    IEnumerable<string> _text = null;
    if (_userComment[_userComment.Length - 2].Contains("Approved by "))
    {
        _text = GetSubStrings(_userComment[_userComment.Length - 2], "Approved by ", " on ");
    }
    else
    {
        _text = GetSubStrings(_userComment[_userComment.Length - 2], "Rejected by ", " on ");
    }

    string _username = "";

    foreach (string item in _text)
    {
        _username = item;
    }
    
    return _username;
}


//This is regex for extracting the desired string

private static IEnumerable<string> GetSubStrings(string inputComment, string start, string end)
{
     Regex r = new Regex(Regex.Escape(start) + "(.*?)" + Regex.Escape(end));
     MatchCollection matches = r.Matches(inputComment);
     foreach (Match match in matches)
         yield return match.Groups[1].Value;
}
 

       
I know that this looks pretty odd, but, it works. I'll try to explain what I am doing here. 

Let's say that we have a task that needs to be approved (or rejected) by 3 different approvers. First user (let's say his name is Rajesh Koothrappali :) ) approves task. 
Now, ConsolidatedComments field looks something like this:

"Approved by Rajesh Koothrappali on 05/05/2013"

These methods will extract Rajesh Koothrappali from this string.

Now, second user (let's say his name is Howard Wolowitz) approves the task. Now, ConsolidatedComments field looks something like this (it is appended to the last string): 

"Approved by Rajesh Koothrappali on 05/05/2013;Approved by Howard Wolowitz on 05/05/2013"

Our methods will now extract Howard Wolowitz from this string.

Finally, third user, Sheldon Cooper comes and he rejects the task. Now, ConsolidatedComments field looks something like this (it is appended to the last string):

"Approved by Rajesh Koothrappali on 05/05/2013;Approved by Howard Wolowitz on 06/05/2013;Approved by Sheldon Cooper on 07/05/2013;"

Our methods will now return Sheldon Cooper as approver.


As you can see, we are always fetching the last username in that string. Hope you will find it helpful.



Tuesday, June 11, 2013

Project Server 2010 - Bug when uploading image on PDP

When adding new Enterprise Custom Fields to Project Server, there aren't many options to choose from what that field is going to be. You can set it to be text, number, duration, flag, cost and date field. You can also set that field to get data from Lookup Table or to be a formula (calculated field).

But, when you need to have a field that will contain an image, you have limited options. One way to get image on your PDP is to create Enterprise Custom Field of type "Text" and for its Custom Attributes choose "Multiple lines of text".

When you add this Custom Field to your PDP, you will have rich textbox in which you can format your text in a way you want and you have the tool (in Ribbon) to upload images to that textbox.



But, if just upload image and click on "Save" button, your image will disappear from the page and it will not be saved!

I've tried this couple of times, and every time, image is not saved.

But, when I add some text below the image (in the same textbox), then the image is saved in that textbox.

I consider this as a bug of Project Server 2010.

Thursday, May 16, 2013

SharePoint - Iterate through large list

Retrieving list items from SharePoint list from code is the most basic thing, and every SharePoint developer is familiar with it.

This is the easiest way for retrieving items of a SharePoint list from code:


using(SPSite siteColl = new SPSite("SiteName"))
{
    using(SPWeb web = siteColl.OpenWeb())

        SPList list = web.RootWeb.Lists["ListName"];
        SPListItemCollection items = list.Items;
 
        foreach (SPListItem listItem in items)
        {
            Response.Write("Item title: " + listItem["Title"].ToString());
        } 
    }
}

But, this is a very poor solution, because this row 
SPListItemCollection items = list.Items; 
returns all items of a list at once. If your list has large amount of items (for example, more then 2000), then this can be a serious performance issue.If you have extremely large list, like 1 000 000 items, this code will certainly crash your server.


The solution is to using SPQuery class and its RowLimit property like this:

using(SPSite siteColl = new SPSite("SiteName"))
{
    using(SPWeb web = siteColl.OpenWeb())

        SPList list = web.RootWeb.Lists["ListName"];
 
        SPQuery query = new SPQuery();
        query.Query = "<Query><OrderBy><FieldRef Name='ID' /></OrderBy></Query>";
        //Scope="Recursive" retrieves items from all folders and subfolders in a list
        query.ViewAttributes = "Scope=\"Recursive\"";
        query.RowLimit = 100;

        do
        {
            SPListItemCollection items = list.GetItems(query);

            foreach (SPListItem listItem in items)
            {
                Response.Write("Item title: " + listItem["Title"].ToString());
            }

            query.ListItemCollectionPosition = items.ListItemCollectionPosition;

        } while (query.ListItemCollectionPosition != null);
        
    }
}


With SPQuery, execution is faster. This query returns all fields from a list, but you can make it even faster if you define only the fields that you need, and not all of them. RowLimit property of SPQuery class ensures that only specific number of items will be processed in one iteration.

Tuesday, May 7, 2013

Project Server 2010 Workflow - AppSettings Part II

In one of my previous posts, I've shown how to read Application Settings from workflow. 

But, that isn't always the best practice. It isn't recommended for users to edit machine.config, especially if you need to store a large amount of data in Application Settings.

Instead of tampering with machine.config, you can just open new SharePoint site in your workflow and instance configuration file of that Web Application. Here is how:





using (SPSite site = new SPSite(contextInfo.SiteGuid))
{
      System.Configuration.Configuration _config = WebConfigurationManager.OpenWebConfiguration("/", site.WebApplication.Name);
      
      string _MySetting = _config.AppSettings.Settings["MySettingName"].Value;
                   
}


Now, in a very simple way, we have read value of setting ("MySettingName") stored in Application Settings of our Web Application.