Making Sense of the SharePoint World

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!


Jun-172009

Sample Chapters Online

MCj04379900000[1]A Taste of "Professional SharePoint Designer"

I am pleased to announce that Microsoft has posted two chapters from "Professional Microsoft Office SharePoint Designer 2007" on MSDN. This is the book I spent the better part of last year writing, along with Asif Rehmani and Bryan Phillips. In the approximately six months since its release, Pro SPD has received excellent reviews, now you can see why for yourselves with the following chapters:

Chapter 11: Advanced Data Access: External Data and More

Chapter 15: Creating Workflow Elements in Visual Studio

These two chapters give you a taste of the breadth and depth you will find in the book, including creating views of complex hierarchical data from Web services, and how to extend the power of SharePoint Designer's workflow capability with custom Actions.

Now that Microsoft has made SharePoint Designer available as a free download, having good documentation available is even more important than ever. I'm sure once you have checked out the samples, you're going to want to buy the rest!


Jan-142009

Press F1 - SharePoint Help is on the Way

MCBD19644_0000[1]Giving Your SharePoint Users Site-Specific Online Help

"Press F1 for Help" has been ingrained into the psyche's of PC users since even before Windows. OK, WordPerfect users originally had to press F3 instead, but the concept is the same.

In any case, SharePoint allows you to take advantage of this convention through its page event model. Page-level scripting in SharePoint has received a big boost in attention recently. Ever since Microsoft has announced its endorsement of JQuery in Visual Studio, articles are popping up all over as people learn to leverage this new framework. This is not one of those articles.

Introducing the WPSC

Instead, this is about one use of a component that has been a part of SharePoint (under various names) since it was called the Digital Dashboard Resource Kit - The Web Part Page Services Component (WPSC). I devote a whole chapter in my book to using this and other client-side components, but briefly, the WPSC is a set of JavaScript objects and functions that allow Web Parts to communicate with SharePoint, each other, and your users.

One of these functions allows you to register for events. These events can be defined in other Web Parts, or pre-defined by SharePoint. This example uses one of the pre-defined events - "onhelp". In essence, the WPSC watches for users to press the F1 key, and then raises this event so that any interested components on the page can respond to it.

Making it Happen

In general, working with a WPSC event requires two things:

  1. Registration for the event
  2. A function to execute when the event is raised (this is called an "event handler" or a "callback function")

For a help function, you will also want a page to contain the help information. You can create this page any way you like, but you might find it useful to make a Wiki Library for your help files. This makes them easy to update and expand as needed. For purposes of this example, let's assume you have a Wiki Library called "MyHelpLib", and the primary help page for this topic is "MyHelp.aspx".

  1. The first thing you need to do is add a Content Editor Web Part to your page. From the Site Actions menu, select Edit Page:
    image
  2. Select a Web Part Zone, and cick the Add a Web Part link.
  3. From the Add Web Parts dialog, select the Content Editor Web Part, and click the Add button. (It will probably be in the "Miscellaneous" section)
    image 
  4. Once the part is added, click the "open the tool pane" link in the part:
    image
  5. Instead of clicking the "Rich Text Editor" button, we're going to click the "Source Editor" button.
    image
  6. The Source Editor is just a text entry window, with a Save and a Cancel button.
    image
  7. The code shown in the screen shot is correct, but here it is in plain-text form, which you can copy and paste into your text-entry window: <script language="javascript">
    function showMyHelpPage() {
    window.open('/MyHelpLib/MyHelp.aspx','MyApplicationHelp')
    }
    WPSC.RegisterForEvent("urn:schemas-microsoft-com:dhtml","onhelp",showMyHelpPage);
    </script>
  8. Click Save.
  9. There is one more change to make before we're ready to exit edit mode. So that this web part doesn't show up on the page, you want expand the Appearance section of the task bar, and change the the "Chrome Type" to None.
    image
  10. Click the Apply button, and the Exit Edit Mode link.

At its most basic, that's all there is to it! Pressing the F1 key on your page will now summon the help page you defined.

Taking it Further

Of course, that doesn't prevent you from getting even fancier. For example, you may want to set some parameters so your help window is a certain size, and centered on the screen. Once you have the basics set up the way you like them, you could export the Web Part to a .DWP file, and add it to the site's Web Part Gallery. Or, instead of using the script in a Web Part, you might put it into a master page, so it is available throughout your site without further intervention.

The following script is one you might place in shared Web Part or a master page. It adds the formatting parameters, but it also uses a variable for the "help context".

    <script language="javascript">
     var myHelpContext = '/MyHelpLib/MyHelp.aspx';
    function showMyHelpPage() {
     var width  = 500;
     var height = 400;
     var left   = (screen.width  - width)/2;
     var top    = (screen.height - height)/2;
     var params = 'width='+width+', height='+height;
     params += ', top='+top+', left='+left;
     params += ', directories=no';
     params += ', location=no';
     params += ', menubar=no';
     params += ', resizable=no';
     params += ', scrollbars=no';
     params += ', status=no';
     params += ', toolbar=no';
     newwin=window.open(myHelpContext,'MyApplicationHelp', params);
     }
     WPSC.RegisterForEvent("urn:schemas-microsoft-com:dhtml","onhelp",showMyHelpPage);
    </script>

This myHelpContext variable allows you to override which page is displayed by setting it on individual pages. In a web part, you just edit the myHelpContext variable for the page you are on. If the main script is embedded in a master page, you would just add a Content Editor Web Part (as described above) with a very short script block:

    <script language="javascript">
     myHelpContext = '/MyHelpLib/ThisPageHelp.aspx';
    </script>

In either case, pressing F1 gives that page's help.

Conclusion

In this article, I have given you a taste of the power available to you with the SharePoint client-side scripting model, and the Content Editor Web Part. We used the event model to provide traditional, F1, Help to your users. But the Web Part Page Services Component (WPSC) gives you access to many other functions as well. You can find many of them documented in the Windows SharePoint Services SDK, and you can see more examples of how to use them in my book. I hope you will take this to heart, and discover even more ways of using the SharePoint client-side programming model!


Oct-242008

Welcome to the Hotel...

 California Site DefinitionMCj02972890000[1] - Your Last Resort

"You can check out any time you like...

...but you can never leave."

(Apologies to the Eagles.)

Joel Oleson, ex of Microsoft, and now an independent consultant and speaker, has just published a new article called "Just SAY NO to Creating Custom Site Definitions". In this article, he provides several excellent arguments for not using Site Definitions. This is fully consistent with the article I wrote a few weeks ago - "First Do No Harm - The SharePoint Customization Hippocratic Oath."

In that oath, the last point was "The Definition of Success: If you must use a Site Definition, understand the ramifications." I expanded it as follows:

None of this is to say that Site Definitions are obsolete, or should never be used. You just need to be aware that this is a very fundamental construct. Once you build a site from a site definition, you cannot change it to a different definition later. You can still tweak it with additional features, but you can't change the "kind" of site it is. It will not be a "SharePoint Team Site", easily and transparently upgradable. It will be an "Acme Group Site", or whatever you have called it, and you need to plan for its entire life-cycle (Much like adopting a puppy). There may be extra steps in the upgrade process, for example (though, at this time, we don't know what those steps might be). You may have trouble getting support if the person who created the Site Definition moves on to greener pastures. This may be worth it for your environment, but it is something you need to take into account.

Elsewhere in that article, I also say that a Site Definition should be pretty close to your last resort. While I have always maintained this position, it is good to see others also coming around.

In many respects, for unwary developers, a custom Site Definition resembles another kind of lodging - euphemistically called a "motel". Site definitions may be attractive, but beware - Sites check in, but they don't check out!

Update:

Ian Morrish has also recently commented on the dangers of using Site Definitions indiscriminately.