Making Sense of the SharePoint World

Nov-272009

FeedBurner Under Control

MCBD07000_0000[1]The Fix is In, Thanks to Tom Resing

I have great news, thanks to fellow SharePointer Tom Resing. In my previous post I mentioned the problems the Community Kit for SharePoint:Enhanced Blog Edition has with the new link tracking parameters FeedBurner just started adding to their links.

In that post, I talked about the trials and tribulations of trying to get CKS:EBE working by installing an updated version. It turns out there was another approach to the problem. Although Google made the change to FeedBurner effective by default, Tom pointed out that they do offer an option to turn it off.

The Quick Fix

So, for those of you using both FeedBurner and CKS:EBE, here's the scoop. On the left menu in your FeedBurner Feed Stats Dashboard, in the Services section, is an option called "Configure Stats":

image

When you select Configure Stats, you have a section called "Reach", which has several options. You need to uncheck the box for "Track Clicks as a traffic source in Google Analytics":

image

That's all there is to it! Save the settings, and everything should be back to "normal".

Of course, it would have been nice if Google had posted a more conspicuous notice that they were making this change, and where it could be configured. It would have been even nicer if they had made the change "opt in" instead of "opt out".

Nevertheless, what's done is done. You should now be able to click through from my RSS feed directly to the articles you are interested in.

I apologize for any inconvenience.


Nov-252009

Burned by FeedBurner

MCj04314980000[1]At Least They Didn't Burn the Turkey

Just a quick note before I run off for the Thanksgiving holiday (USA). If you have been a subscriber to my RSS feed, you may have noticed a problem clicking through to my blog posts lately. This is because of a change that FeedBurner made a few weeks ago. They added extra parameter information to the connection string of links back to the blog.

This is theoretically a good thing, as it allows site logging to better determine where visitors are coming from. However, this blog uses the Community Kit for SharePoint: Enhanced Blogging Edition (CKS:EBE). The way CKS:EBE handles URLs doesn't allow it to correctly interpret these additional parameters. This resulted in broken page displays. You can still eventually navigate back to the right page, but it isn't as convenient as it should be.

I have just tested a patched version of CKS:EBE to resolve that problem. While the patch for the FeedBurner problem seems to work correctly, there are significant issues with other changes to the patched build of CKS:EBE. I noticed that my tag cloud was no longer resizing the keyword links to their proportions, for example, and there were major authentication problems. These glitches are bad enough that I decided to retract the update.

I apologize for the inconvenience. Rest assured, I'll continue working on getting links from FeedBurner working (without breaking everything else).

In the mean time, Have a Happy Turkey Day!


Nov-112009

A Free SharePoint Org Chart Web Part

MCj04348220000[1]Enhancing a CodePlex Project

In this article, I'm going to get a bit more technical than is my normal wont. In fact, I'm going to go into Visual Studio, and show you some C# code as well.

(Waiting a few minutes for folks who know me to pick their jaws up off the floor, or even find some smelling salts... Yes, Virginia, I really can program when I have to. :) I just very rarely have to.)

That doesn't mean I'm going to be building this project from scratch. Far from it. So, if you're looking for info on how to build a .wsp solution package, this isn't the article for you. I am going to talk a bit about user choice, web part properties, a programming technique called "recursion", and the SharePoint API (in particular, accessing the User Profile store).

What I'm going to do is show you how to fulfill a very common business requirement by starting with a fairly simple CodePlex project, and tweaking it until it is something that does just what you need.

A Common Request

Virtually every client I've implemented SharePoint for has asked for an organizational chart. The problem is, there aren't very many org chart web parts out there for SharePoint, and those that are, tend to have issues (usually performance) - especially if you have a fairly large organization.

SharePoint Server 2010 finally makes a really cool organization chart part of the package. It is Silverlight based, and works pretty fast. Unfortunately, that's 2010, and it is going to be a while before many companies are ready to deploy it. We need something that will work for "today".

Finding the CodePlex Project

So what do you do? Well, a quick online search reveals that there is indeed an org chart part on CodePlex, called (quite originally) "OrgChartPart". This was written by Rodney Viana. Looking at the specs, it sounds promising:

  • It reads profile information from SharePoint
  • It uses a free JavaScript Org Chart rendering engine
  • And of course, it is free, too!

What's not to like?

There is a bit, as it turns out, but let's start with the good. Installing the OrgChartPart is a breeze. Download the .wsp file from CodePlex, install it with the "stsadm -o addsolution" command, and deploy it to your web application(s). You will then need to add the part to your web part gallery (there is a feature that does this, or you can do it manually), then add it to a page. (I suggest you add it to a web part page that has nothing else on it, for reasons you will see in a moment.) This takes 5 minutes, tops.

The web part reads your MOSS profile store, and emits the chart. Just like that, you're done!

On the surface, that doesn't sound too bad. But dig a little deeper, and you find a big red flag - the author warns that you shouldn't use the part if you have more than 500 profiles in your organization. Of course, if you have more than 500, you've got a problem.

But even if you have fewer than 500 profiles, you might not like the results. Consider the following clip of the chart initially rendered by this part:

image

This is a pretty complicated organization - it doesn't fit on the screen, and it is completely expanded by default. But, there's another issue. The chart looks a little "top heavy". That's because the Active Directory includes a number of accounts that aren't actually users, and therefore don't have associations in the hierarchy. Looking closer, you can see that Patricia Doyle, the CEO, has conference rooms as peers! There's also some clipping of titles and departments.

image

Note: This is technically a problem with the Active Directory and/or its import. In theory, you could set up the AD/Profile import to exclude non-person entities. But, that's another story... :)

Looking Under the Hood

Still, even with those issues, you can see the potential in this part.

Note: Most of the remainder of this article discusses how the OrgChartPart was originally implemented, and how I updated it. If you just want the end results, you can download the original source from CodePlex, plus my replacement OrgChartPart.cs file and rebuild the solution. (I have also given the code updates to Rodney, who may update the actual CodePlex project.)

Fortunately, as with most CodePlex projects, we have access to the source code. I downloaded the source and opened the project in Visual Studio. What we see is, the part is actually quite simple. There is one .cs file (the two in the image below include my edited version, plus the original) and the ECOTree JavaScript library. ECOTree is what actually does the heavy lifting of drawing the chart. By default, OrtChartPart uses just a fraction of the power of this library (follow the link above for more details).

image

There is also a "departmentconfig.xml" file hiding in the Layouts folder of the 12 hive. Hmmm...

Looking at the code itself, we find there are four main functional areas beyond the basics needed to render a web part:

  1. Handling that department file
  2. Crawling the profile store
  3. Building a profile render block
  4. Building the chart

Now, We Tweak

In the rest of this article, I'm going to look at each of the sections I described above, tell you how the original code worked, and show you what I did to update them. While I won't embed the entire source in the article, you can easily download it if you want to examine everything in context.

On the Department File

For now, I'm just going to say that while the idea of mapping the department information in the profile to department home pages is admirable, the way it was implemented was causing some serious performance issues, and would be a long-term maintenance headache. Therefore, I commented out the two lines that actually make use of it (249 and 267 in my updated file), and replaced the link rendering with a simple text rendering. Here are the original and replaced lines from the updated code ( 267, 268).

//this.department = config.Find(EmptyIfNull(Profile[PropertyConstants.Department]));
this.department = EmptyIfNull(Profile[PropertyConstants.Department]);

"config.Find" is part of a config object that is defined elsewhere in the code. For purposes of this article, though, we're just going to ignore it.

Crawling the Profile Store

The profile crawl itself takes place in a function called PopulateOrg(). Examining this routine, a few things stand out.

First, there are two different ways for nodes to be added to the chart. One uses information read from a user's profile, the other feeds static information into a node. (This is used primarily to display an error message, if needed.)

Second, the population routine reads the entire profile store in one fell swoop. Combined with the inefficiency of the department link mapping mentioned above, this is a big part of the performance limiting the part to organizations with 500 or fewer profiles.

Finally, hidden in this routine (but commented out) is a nice little set of sample data.

At its core, an organization is a hierarchy. A hierarchy is made of parents and children. Technically, as long as you know the parent of each node, you can manually construct the organization. This is the approach originally taken by this web part.

foreach (UserProfile pf in pmManager)
{
     Details dt = new Details();
     dt.AddProfile(pf, this.Page.Request);

     if (dt.TrimFullName != String.Empty)
         employeeList.Add(dt);

}

However, a UserProfile object itself has methods for listing its children and ancestors directly. This means that we can easily start at any point in the tree, and generate the organization descendent from it, as well as the chain of command above it.

The ancestors are represented in a single collection, called through the GetManagers() method. The first level of children are just as easily retrieved with the GetDirectReports() method. However, if we want more than one level of child, we would then need to call the GetDirectReports() method for each child returned in the previous call.

If you try doing this with loops, things can get complicated pretty quickly. However, there is a simple way to to handle an arbitrary number of child nodes, to an arbitrary depth. This is a technique called "recursion". Essentially, you create a function that calls itself. In order to keep it from running forever, one of the parameters that should be passed to a recursive function should be a "depth" limiter. Within the function, the child calls decrement this limiter, and stop calling when it reaches a threshold level. In my update, I create the recursive function addChildren(). This takes the starting profile, and the depth limiter as parameters.

protected void addChildren(UserProfile Parent, int levels)
{
    try
    {
        foreach (UserProfile pf in Parent.GetDirectReports())
        { 
           Details dt = new Details();
            dt.AddProfile(pf, this.Page.Request, false);

            if (dt.TrimFullName != String.Empty)
            {
                employeeList.Add(dt);
                if (levels > 0)
                    addChildren(pf, levels - 1);
            }
        }
    }
    catch (Exception ex)
    {
        AddInList("1", "", "Error", "", ex.Message); 
    }
}

Notice also that I have added a parameter to the AddProfile method of the details (dt) object. This third parameter allows me to force the profile being added to act as a root, or top-level, node. (This is used for the management chain of command.)

Giving Your Users Choices

Looking at the stuff I've described in the preceding section, there is something very different from the original implementation - instead of crawling the whole profile store, I crawl a specified subsection. That requires two pieces of input - the node I plan to start with, and how deep I want to crawl.

Fortunately, when you create SharePoint Web parts, you can easily allow the user to enter configuration information. In this case, I am actually going to give the user three options. The third will allow the user to show the sample data instead of the active directory. (Naturally, I uncommented the sample population section, and wrapped the two population methods in an if...else block.)

To add a property to a web part, you add some descriptive information to say how the property is to be displayed. You also set up a public property variable to appear in the configuration pane, and a private variable to use in your code. The private variable can include a default value. The public property includes a get and a set block. While these three properties simply take the value as given, you will see later that this isn't the only option.

[Personalizable(PersonalizationScope.Shared)]
[WebBrowsable(true)]
[System.ComponentModel.Category("Org Chart Settings")]
[WebDisplayName("StartProfile")]
[WebDescription("Enter the top user to display:")]
public string StartProfile
{
    get
    {
        return startProfile;
    }
    set { startProfile = value; }
}
private string startProfile;

[Personalizable(PersonalizationScope.Shared)]
[WebBrowsable(true)]
[System.ComponentModel.Category("Org Chart Settings")]
[WebDisplayName("ChartDepth")]
[WebDescription("Subordinate levels to display:")]
public int ChartDepth
{
    get
    {
        return chartDepth;
    }
    set { chartDepth = value; }
}
private int chartDepth = 1;

[Personalizable(PersonalizationScope.Shared)]
[WebBrowsable(true)]
[System.ComponentModel.Category("Org Chart Settings")]
[WebDisplayName("Show Sample Data")]
[WebDescription("Show sample data instead of profile info:")]
public bool ShowSample
{
    get
    {
        return showSample;
    }
    set { showSample = value; }
}
private bool showSample = true;

This is how the properties look in the Web part configuration pane. The "CacheTime" parameter was included in the original part (though it is unused in the code). I'll discuss the "Left To Right" parameter later.

image

You might also notice that I set the default value of showSample to true. This allows the user to place the web part on the page and see a reasonable representation of its function, without needing to first set the start point and depth.

image 

Note: The chart initially renders with only the first layer of children open. I expanded it for this screen shot.

Rendering a Profile

Each node on the chart is constructed of HTML. The HTML is assembled in the ReturnItem() function.

I wanted to enable two functions not already present - showing the user's profile picture, and allowing the user to "re-home" the chart on another node in order to enable navigation of the entire organization in convenient sections. This meant I needed to add two properties to the details object, and then add the code to display these properties appropriately in the chart.

In order to make the re-home function work, I added an override to the StartProfile. Essentially, if there is a rootuser query string parameter, the profile specified there will be used instead of the one set in the web part property. The SetRoot link calls the current page with rootuser set to the selected profile.

In addition to the actual HTML, however, the look of the node contents are controlled by styles defined in the ECOTree.css file. In order to allow the picture to render, as well as permitting the text to fit, I needed to modify the .econode class in this file as follows.

.econode {
    text-overflow: clip;
    font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
    font-size: xx-small;
        color: White;
    padding: 2px;
}

Rendering the Chart

Just as we built up a node by assembling the HTML, the chart itself is built by assembling JavaScript. One of the things I did was to examine the functions provided by the ECOTree library. I found that it offered a lot of flexibility that wasn't currently in use. I felt the most important was the ability to set the orientation. Here's where that "Left to Right" checkbox comes into play. The library uses a parameter .RO_TOP or RO_LEFT to decide whether the chart is top to bottom, or left to right.

This is the code that reads and sets that web part parameter:

[Personalizable(PersonalizationScope.Shared)]
[WebBrowsable(true)]
[System.ComponentModel.Category("Org Chart Settings")]
[WebDisplayName("Left To Right")]
[WebDescription("Root at left, otherwise at top:")]
public bool RootLeft
{
    get
    {
        if (chartOrient == "LEFT")
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    set
    {
        if (value)
        {
            chartOrient = "LEFT";
        }
        else
        {
            chartOrient = "TOP";
        }
    }
}
private string chartOrient = "TOP";

This demonstrates the kind of things you can do with the get and set blocks of a parameter. The user gets to the see the checkbox that represents a binary choice, but the internal variable is a string, meaning that we can render the part simply appending the string value without inserting an "if" block.

myTree.config.iRootOrientation = ECOTree.RO_");
sb.Append(chartOrient);
sb.Append(@";

The Results

The image below shows you how the revised chart looks with the same profile store shown in the first image.

image

This is a much more manageable presentation. It allows the user to drill into the organization as needed, and since we set the starting node on our CEO, those conference rooms who don't report to anybody are nowhere to be found.

I hope this article has shown you how easy it is to stand upon the shoulders of giants when looking for features to add to SharePoint. I built upon the CodePlex OrgChartPart, which itself leveraged the ECOTree charting library.

There are many more things you could do with this web part. You could add options to select the color of the nodes, for example. Or give users a check-box to render the entire organization at once, as the original part did.

The options are limited only by your imagination!


Sep-32009

Calculated Columns and SPD Workflow Part 2

MCj02317860000[1]In this two part series, I show you how to take advantage of SharePoint calculated columns in your SharePoint Designer workflows. Part 1 introduced calculated columns. Part 2 will describe several string manipulation functions, and show you how to consolidate these calculated columns into a "function library" of sorts.

Hide the Clutter

In part 1, I showed you how to use a calculated column to enhance a SharePoint Designer workflow. This is all well and good, but what if you have interim calculations? What if you need to do this manipulation on values entered by your user in the workflow initiation form?  Or maybe you have other types of information you don't want getting in the way of your users when they view the list.

This is where that "Function Library" I talked about comes into play. Technically, in SharePoint terms, it is actually a function list. We will create a list in SharePoint that contains not just the calculated columns, but "input" columns for the functions as well. We will then hide the list in SharePoint designer. Your workflows can still access it, but your users won't even know it is there.

Holiday for Strings

String manipulation is a great application for calculated columns.

One common workflow scenario is processing the items sent to an email-enabled list. Typically, you will want to parse the subject line to find some routing information. This information may take several forms, including:

  • The beginning of the subject up to a delimiter
  • The end of the subject following a delimiter
  • The information contained within a pair of delimiters
  • Everything except what is contained between a pair of delimiters

Of course, there are other possibilities, but these should be enough to illustrate the concept.

Building the List

We're going to create a list that supports all of the calculations described in the previous section. To start, of course, we need to create the list. (Remember, SharePoint Designer 2007 doesn't have the ability to create or edit the columns in a MOSS 2007/WSS 3.0 list, so we'll do this in the web UI.)

  1. From Site Actions, select "Create"
  2. From the Create page, select "Custom List"
  3. Give it the name TextFormulas. Since we're ultimately going to hid the list, don't show it on the QuickLaunch
    image
  4. Click Create

Now that we have a basic list, we need to create our input columns:

  1. Display the newly-created TextFormulas list.
  2. From the Settings toolbar, select List Settings
    image
  3. There is a "Title" column created by default. Click on it, change the name to "SourceString", and click OK.
    Note: This step isn't technically required, but it helps things make sense in the Workflow Designer
    image
  4. Click Create Column
  5. Enter "Delimiter1" for the Column Name.
  6. Make sure "Single line of text" is selected as the field type, and click OK.
    image
  7. Repeat steps 4-6, except use "Delimiter2" as the column name.

Once you have your input columns defined, you might think it is time to create your calculated columns. While you could, there is one more step you might want to perform. Because the output from string calculations isn't always obvious, it can be helpful to have some sample data to work with so you can see if the formulas are doing what you want them to.

Add an item to your list:

image

This item will give you all of the possibilities that we might want to work with - text before, after, within, and outside of, delimiters.

Now we're ready to create the calculated columns.

For the first column we just want the text to the left of the first delimiter. This has the simplest formula, but it isn't necessarily as obvious as you might think. The formula is "trim(left([SourceString],find([Delimiter1],[SourceString])-1))"

We're using three string functions: Trim(), Left(), and Find(). They're also "nested", meaning that we're calling one function from within another function.

The obvious function is "Left()". It takes two parameters, the string we want the left hand side of, and how much of the string we want. Unfortunately, since this is delimited rather than a fixed position, we need to "Find()" the position of the delimiter in the source string.

That's all well and good, but why are we then subtracting 1? That's because we would otherwise return the delimiter itself in our results. Find returns the position of the delimiter, and Left function expects a count of returned characters. Since we don't want the delimiter, we subtract 1 so that we get the position immediately before it.

The Trim() function gets rid of any leading and trailing spaces, as even if you don't see them, these can make comparisons fail.

Create your calculated columns in accordance with this table:

Column Name Formula
BeforeDelimiter1 trim(left([SourceString],find([Delimiter1],[SourceString])-1))
AfterDelimiter1 trim(right([SourceString],len([SourceString])-len([BeforeDelimiter1])-2)
BetweenDelimiters trim(left([AfterDelimiter1],find([Delimiter2],[AfterDelimiter1])-1))
AfterDelimiter2 trim(right([AfterDelimiter1],len([AfterDelimiter1])-len([BetweenDelimiters])-2)
OutsideDelimiters [BeforeDelimiter1]&" "&[AfterDelimiter2]

Notice that each formula builds on the one before it - we're using the results of some calculated columns as input to others. This helps us avoid the 8-deep function nesting limit, as well as the 1000 character formula length limit. It also makes them a lot easier to read!

Assuming the formulas are entered correctly, viewing your sample data item will give you these results:

image

While the value for "AfterDelimiter1" doesn't look very useful in and of itself, that is only because of the particular SourceString we are using. In this case, it is simply an interim value for deriving the BetweenDelimiters result. With a different SourceString it could be the final answer you are looking for. For example, you may have a simple two-part source with a single delimiter, such as "G131131|Memory Failure". That string, with just the pipe (|) as Delimiter1, will result in "Memory Failure" for the AfterDelimiter1 value.

Back to SharePoint Designer

Once you have your function list created, you can use it in your SharePoint Designer workflows.

For this example, we're going to create a workflow on an email-enabled Time Off list. When a new item arrives, we want to parse the subject line to get the reason for the absence and assign it to the reason field. The reason text follows a dash (-), so we're just going to use the "AfterDelimiter1" calculated value.

Note: You may need to enable email-based workflows with the following command before using this example: "stsadm -o setproperty -pn declarativeworkflowautostartonemailenabled -pv true" See this KB Article for details.

When you define your workflow, first check the "Automatically start this workflow when a new item is created box, as shown below:

image

Click Next.

From the Actions menu, select Create List Item. (Since the menu is built from recently used actions, you may need to select it from the "More Actions" dialog.) You will get a new line in the Actions block that looks like the one below. Click the "this list" link.

 image

Select the TextFormulas list from the dropdown:

image

The SourceString (*) field will already be chosen, as it is a required field. (This will be the "Title" field if you didn't rename it earlier.)

Click the "Modify..." button. You want to use the E-Mail Subject field from the current item as the source.

image

After you click OK, Click Add in the Create New List Item dialog, and add the Delimiter1 field. Enter the dash as the Value. When you click OK, the Create dialog should look like this:

 image

Click OK. Notice SPD automatically generates a variable called "Create" as the output of this function. That variable will hold the item ID for the formula item we create. This will be needed later.

A Pregnant Pause

The next action may seem a little odd. We're going to select the "Pause for Duration" action, and then set it to 0 days, 0 hours, and 0 minutes. The reason for this is that the calculations in our function list item don't take place instantaneously. If we were to try to grab the result as the next step, all we would get is an empty string. By telling our workflow to Pause, we give SharePoint a chance to catch up.

Note: Even though we set everything in the pause to zero, the workflow will wait until the next event cycle to continue. This delay may be a minute or so.

Back to Work

Once the workflow comes back from its coffee break, we need to get our value back.

  1. From the Actions menu, select Set Field in Current Item.
  2. Click the "field" link, and select Reason from the dropdown.
  3. Click the "value" link, then click the fx button that appears.
  4. Select TextFormulas for the Source, with the Field of AfterDelimiter1.

Now we need to tell the workflow how to find the row we want. Here's where that "create" variable comes into play.

  1. Select the TextFormulas:ID field from the Field dropdown
  2. Click the fx button beside the Value field.
  3. Select Workflow Data for the Source, and Varable: create for the Field
  4. If your lookup dialog looks like the one below, click OK
    image

The reason lookup should now look like the capture below:

image

Click OK.

Cleaning Up

Now it is time to clean up after ourselves.

Just as "real" SharePoint developers need to keep in mind the need to "dispose" objects they create once they are done with them, we need to delete the formula record we created now that we are done with it. Fortunately, SharePoint provides a "Delete Item" action item for us. Select it from the Actions menu, and click the "this list" link. As you might guess by now, we're going to use the same "create" variable as before to select the item to delete from the TextFormulas list.

image

Click OK, then click Finish in the Workflow Designer. SharePoint Designer will then validate the workflow and save it.

Covering our Tracks

Finally, as I mentioned way back at the beginning of this article, you may not want your users to readily see your formula list. Especially since the interim information will be hanging around during that pause. Although we suppressed the list from the Quick Launch when we created it, people can still see it from the "View All Site Content" link. To make it go away from there, we can take advantage of SharePoint Designer's ability to "hide" a list.

To hide a list, first open the Lists folder (you won't need this step for document libraries).

Right-click the list you want to hide and select Properties, as shown below:

image

Click the "General" tab. Click on the checkbox labeled "Hide from browsers".

Click OK

image

Now the list will be "invisible" via most normal ways of discovering SharePoint content. That is also why we waited until after the workflow was done in order to hide it. Although you can still see and work with hidden lists in most parts of SharePoint Designer, the function called by the Workflow Designer to enumerate lists is the same one used by the web interface, so you wouldn't be able to select it.

Conclusion

This second part took a bit longer to write than I expected. I hope you found it worth the wait!

In these two articles, I have showed you the power of SharePoint calculated columns, and how to use them in a SharePoint Designer workflow. In the process, I have introduced a number of other concepts, including:

  • String functions
  • Hidden lists
  • Email enabling lists
  • Using the output of one calculated column to feed another.
  • Workflow variables

Even so, I have barely scratched the surface. I encourage you to explore further the capabilities of calculated columns and SharePoint Designer workflows.