zevenseas


 

FeaturePushdownJob

While I was browsing through the Central Administration Page of SharePoint 2010 I couldn’t help noticing the screen you can below:

image

Basically it’s a page when you upgrade from SharePoint Foundation to SharePoint Server and you wish to activate all the new features that you get extra with the new license. But wouldn’t it be great if you set your own set of custom features in there to make sure that your features are automatically installed at every sitecollection and site? I created such a timerjob myself for the LCM tool to install an eventhandler to each web and site within a WebApplication, but I rather trust Microsoft applying these changes then if my code does ;)

I opened up Reflector to see what is being used behind the scenes, turns out that there is a specific timerjob that executes and loops through every WebApplication and then SiteCollection and every Site. But when I tried to instantiate the class in my project it immediately says :

image

Aargh! Well.. maybe we can see what it’s internally using. Seems it’s using the FeaturePushDownManager class that’s part of the Microsoft.Office.Server namespace, let’s open that with Reflector to see what it is about..

internal sealed class FeaturePushdownManager
{
    // Methods
    public FeaturePushdownManager();
    internal static void ActivateSiteScopedFeature(SPSite site, Guid featureId);
    internal static bool ActivateSiteScopedFeature(SPSite site, Guid[] featureIds, bool eatException);
    internal static void ActivateWebApplicationScopedFeature(SPWebApplication webApplication, Guid featureId);
    internal static bool ActivateWebApplicationScopedFeature(SPWebApplication webApplication, Guid[] featureIds, bool eatException);
    internal static void ActivateWebScopedFeature(SPWeb web, Guid featureId);
    internal static bool ActivateWebScopedFeature(SPWeb web, Guid[] featureIds, bool eatException);
    internal static bool FeatureStaplingAllowed(SPSite site, SPWeb rootWeb);
}

Aaarrgh.. again an internal method we can’t (re) use!

Are you thinking the same thing I’m thinking? We should make this public and available for re use in our code! Yes.. I see a nice little Codeplex project in the near future (if in the mean time this nice little class isn’t made public in the RTM version)! What do you think?

I’m a SharePoint Services MVP!

Congratulations! We are pleased to present you with the 2010 Microsoft® MVP Award! This award is given to exceptional technical community leaders who actively share their high quality, real world expertise with others. We appreciate your outstanding contributions in SharePoint Services technical communities during the past year

How awesome is that to get in your inbox eh? I’ll tell you.. it’s bloody awesome ! (except for the fact that it appeared in my junk folder (of my BPOS account :))

So hereby I want thank everyone who reads my blog (this one and my old one), checked out my codeplex projects or contacted me on messenger, I hope that you gained something out of it!

I have a lot of fun writing here though sometimes I wonder if the message comes across (apparently it does sometimes ;)

 

Right.. so.. time for a beer now!

SP.UI.Notify is awesome

When I saw the notifications in 2010 for the first time, I really thought that could be used for some very cool stuff. For example, wouldn’t it be cool to set up a message somewhere centrally like “Servers are going down this weekend for maintenance” and everybody sees that notification in their browser.

Well, I managed to get such a nice little app working (you wouldn’t have thought that eh? ;) So what did was the following:

  • Create a control that displays the message based on the centrally managed setting
  • Create an application page where you can set the message and other properties.
  • Make use of the AdditionalPageHead delegate to load the control on every page

Below are some screenies how it looks:

notify 

notifyteamsite

I had some problems to display the notification on the load of the page, since it was working on postbacks. There was something going on with the order of loading of the several javascripts. While debugging the page using the IE Developer Toolbar it seemed that only two classes of the SP.UI namespace were loaded (and thus not the Notify one). So I had to figure a way on how to make my control display the notification after the SP.js was loaded. Luckily my buddy Waldek had came across such a problem before and told me to make use of the following JavaScript function “ExecuteOrDelayUntilScriptLoaded(‘functionToExecuteAfterScriptIsLoaded’, ‘javascriptFileToLoad’)”, and that the trick! :)

Here is the source of the UserControl where I add javascript to the page that loads the Notification:

protected override void CreateChildControls()
{
    SPWebApplication webApplication = SPContext.Current.Site.WebApplication;
    Notification notification = webApplication.GetChild<Notification>("Notify");
    if (notification.Display)
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.AppendLine("<script>");
        //First wait until the SP.js is loaded, otherwise the Notification doesn't work and we get an null reference exception
        stringBuilder.AppendLine("ExecuteOrDelayUntilScriptLoaded(Initialize, \"sp.js\");");
        stringBuilder.AppendLine("function Initialize() {");
        stringBuilder.AppendLine("SP.UI.Notify.addNotification(\"" + notification.Message + "\", " + notification.Sticky.ToString().ToLower() + ", \"Tip\", null);");
        stringBuilder.AppendLine("}");
        stringBuilder.AppendLine("</script>");
        this.Controls.Add(new LiteralControl(stringBuilder.ToString()));
    }
    base.CreateChildControls();
}

Pretty neat eh? I’m not really a big fan of adding a script like I just did but at least it get’s the job done… the complete solution is on CodePlex, so if you’re interested to take a look at it be my guest!

For the next release/update I want to work on :

  • Asynchronous updates
  • Make use of the status bar as well so you can choose to use the Status Bar of the Notifications
  • Make a ribbon action and add that to the WebApplication Ribbon and use a popup for the Settings.aspx

 

Posts that helped me out (thanks guys!) :

 

@Zimmergren : this was the cliff hanger I was tweeting about ;)

Social Computing Overview and a short status update

Social Computing Overview
If you missed us at the SharePoint Connections in Amsterdam that was held on the 18th and 19th of January, here is a chance to see our experimental session about the new social computing features that are available in SharePoint 2010.

So to answer the first question “why is it experimental?’, it is because we brought in a server, two laptops, an extra projector and a hub (something you should actually never do when presenting or doing a demo, especially with beta software ;)

First Daniel talks about what Social Computing really is and what all the fuzz is about (which he does rather excellent if I say so myself :)

image

Then to start the demo.. Daniel writes a blog post and shows us how easy it is to Tag content in 2010..

image

Next it’s demo time for Mark and me.. Mark is showing the new enhancements of the MySite which features the Network page (which shows all the activities you are following from your colleagues and tags) and the Profile page (and during the demo something funny happens with Mark’s laptop battery ;))..

image

Then Mark fires up a OneNote book and saves that into his MySite and the ‘experimental’ is about to start, at that moment I join the session and open up Mark’s OneNote using the new Office WebApps to collaborate with each other real-time (with a 8sec delay..)

image

Hope I got you excited to watch it and see us in action.. if so, then please go to Channel 9 and watch it! ;) Kudos to Matthijs Hoekstra (@mahoekst) from Microsoft for letting us do this experimental demo. Also a huge thanks to the guys responsible for the audio/video who did an excellent job getting all the stuff exactly right (I’d never expected our projector to be captured so good and nice like it did!)

Status update
So what about the status update you might wonder, well recently I finished a project and as of today I have no projects to do. So if you think you can use me on your project then please ping me at robin at zevenseas.com!

Updated : FeatureBlocker

Remember I published the FeatureBlocker tool a couple of months ago? If you don’t, here’s a short description in what the tool does:

Using the FeatureBlocker, you can prevent users from activating features within the SharePoint UI. There are three types of ‘prevent’ actions : block the ‘activate/deactivate’ button, hide the entire feature, redirect the user when clicking on the activate/deactivate button.

Check out zevenseas Feature Blocker and Want to prevent users from activating certain features? about the history of the tool..

So, what’s changed? Well.. by a popular demand (@UncleJohnsBand) I’ve added the option to ‘block’ features per feature-scope.. Next to that I’ve also cleaned up the UI from the Settings page, I moved the configuration of the ‘block’ type into a new page and the creation of a list where you can store the ‘requests’ of the features as well. Below is the screenshot of the configuration page:

image

Let me know what you think of it and if you have additional requirements or feedback don’t hesitate to contact me ;)

Where can you find it? At http://www.codeplex.com/zsfeatureblocker !

Where is the Save button?

As we were prepping for the demo that we held at the SharePoint Connections conference in Amsterdam last week about “Social Computing Overview'”, one of the things that we wanted to demo was the online collaboration bit.

At first, we wanted to show Word 2010 client and the Word 2010 WebApp and the interaction between those two. But, as it turned out, you have to save every time you want to ‘share’ your part of the document to the other person. Then, just for the fun of it, I took a look at OneNote and in particular the OneNote WebApp..

I wrote something down and, of course, I wanted to save my document. First I looked in the upper bar for the well known blue disk icon.. but there was none.. so .. I opened up the “backstage” view (by clicking on “File”) and there I saw the menu option “Where’s the Save Button?”.. now at this moment I was already laughing out loud! Somebody in the Office team must have a good sense of humor to include these kind of bit smart-ass menu options ;)

image

So what happens if you click on it? Well, then you’re confronted with this dialog:

image

It’s surprising in a way why only Word does not have this feature because PowerPoint and Excel WebApp’s also have this ‘automatic’ save functionality. I mean, I can understand why it’s not in Word but for consistency sake it would be good to have it in there by default or something.

Do you live in India.. and do you love SharePoint?

Then wait no longer and contact us at bestinindia@zevenseas.com and send us your resume!

But only do so if :

  1. You have been working with SharePoint for more than 3 years.
  2. You are a C-Sharper.
  3. You are in Information Technology because you love it and can prove it!
  4. You are a great communicator, and a team player.
  5. If you think India (and the Netherlands) beat Australia at cricket! ;)

Love to hear from you!

Current Navigation, SPNavigationNodes and the AreaNavigationSettings page

I was at a customer recently where I had the request that the current navigation of a team site should have a particular ordering. In this ‘current’ navigation, the navigation consisted of links to lists on the specific site to links to sub sites of that site. I knew that in the AreaNavigationSettings.aspx page (see screenshot) you could define such a ordering just by moving links up and down and also check the checkbox to display subsites. So, I thought.. ha that’s easy! I’m just going to check what kind of code is behind that page.. use that et voila.. Robin is a happy man.. :)

image

Boy.. was I wrong.. maybe it was my bad that I couldn’t really figure out what they are doing behind the scenes when I used Reflector to see how the code looked like but maybe it wasn’t ;) So instead of staring at the code and trying to make it work in my dev environment I turned to Google again and did some specific searches and then I came across my lifesaver Gary Lapointe with this post : More Site Navigation Settings Commands.

Apparently, there is no real way of ‘modifying’ the navigation by just moving nodes around and then save the collection if you want make use of the ability of displaying subsites by making use of the IncludeSubSitesInNavigation bool. The thing is that you will have to delete the current nodes and recreate them, in the proper order, and save that back (please correct me if I’m wrong!).

To continue, I used his code and modified it a bit so I wouldn’t need to pass a xml document with Nodes but just a List<Nodes> to create my navigation. Next to that, I was only interested in modifying the Current Navigation and not the Global Navigation so I stripped that bit out.

Here’s the my custom NavigationNode class and the method to create the navigation:

   1: public class NavigationNode
   2: {
   3:     public string Title { get; set; }
   4:     public string Url { get; set; }
   5:     public NodeTypes NodeType { get; set; }
   6:     public SPNavigationNodeCollection NavigationNodeCollection { get; set; }
   7:  
   8:     public NavigationNode(string title, string url, NodeTypes nodeType, SPNavigationNodeCollection collection)
   9:     {
  10:         Title = title;
  11:         Url = url;
  12:         NodeType = nodeType;
  13:         NavigationNodeCollection = collection;
  14:     }
  15: }
   1: protected void CreateNavigation()
   2: {
   3:     PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(CurrentWeb);
   4:     List<NavigationNode> nodeCollection = new List<NavigationNode>();
   5:     nodeCollection.Add(new NavigationNode("Home", "Pages/Default.aspx", NodeTypes.AuthoredLink, publishingWeb.CurrentNavigationNodes));
   6:     nodeCollection.Add(new NavigationNode("Discussions", "Lists/Discussions/AllItems.aspx", NodeTypes.AuthoredLink, publishingWeb.CurrentNavigationNodes));
   7:     nodeCollection.Add(new NavigationNode("Documents", "Shared Documents/Forms/AllItems.aspx", NodeTypes.AuthoredLink, publishingWeb.CurrentNavigationNodes));
   8:     nodeCollection.Add(new NavigationNode("Wiki", CurrentWeb.ServerRelativeUrl + "/Wiki", NodeTypes.Area, publishingWeb.CurrentNavigationNodes));
   9:     nodeCollection.Add(new NavigationNode("Calendar", "Meetings/Lists/Kalender/calendar.aspx", NodeTypes.AuthoredLinkToWeb, publishingWeb.CurrentNavigationNodes));                      
  10:     SetNavigation(CurrentWeb, nodeCollection, true, false, true, true);
  11:     publishingWeb.Update();
  12: }

And then here is my, slightly, modified version of Gary’s awesome code :

   1: /// Original source is from Gary Lapointe, I've only done a modification so that it accepts a List with NavigationNodes instead of a XML file
   2:        /// <summary>
   3:        /// Sets the navigation.
   4:        /// </summary>
   5:        /// <param name="site">The site.</param>
   6:        /// <param name="web">The web site.</param>
   7:        /// <param name="nodeCollection">A list containing all the nodes.</param>
   8:        /// <param name="showSubSites">if set to <c>true</c> [show sub sites].</param>
   9:        /// <param name="showPages">if set to <c>true</c> [show pages].</param>
  10:        /// <param name="deleteExistingGlobal">if set to <c>true</c> [delete existing global nodes].</param>
  11:        /// <param name="deleteExistingCurrent">if set to <c>true</c> [delete existing current nodes].</param>
  12:        public static void SetNavigation(SPWeb web, List<NavigationNode> nodeCollection, bool showSubSites, bool showPages, bool deleteExistingGlobal, bool deleteExistingCurrent)
  13:        {
  14:            PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
  15:  
  16:            // First need to set whether or not we show sub-sites and pages
  17:            pubweb.IncludeSubSitesInNavigation = showSubSites;
  18:            pubweb.IncludePagesInNavigation = showPages;
  19:            pubweb.Update();
  20:  
  21:            List<SPNavigationNode> existingGlobalNodes = new List<SPNavigationNode>();
  22:            List<SPNavigationNode> existingCurrentNodes = new List<SPNavigationNode>();
  23:            // We can't delete the navigation items until we've added the new ones so store the existing 
  24:            // ones for later deletion (note that we don't have to store all of them - just the top level).
  25:            // I have no idea why this is the case - but when I tried to clear everything out first I got
  26:            // all kinds of funky errors that just made no sense to me - this works so....
  27:            foreach (SPNavigationNode node in pubweb.GlobalNavigationNodes)
  28:                existingGlobalNodes.Add(node);
  29:            foreach (SPNavigationNode node in pubweb.CurrentNavigationNodes)
  30:                existingCurrentNodes.Add(node);
  31:  
  32:            List<NavigationNode> newCurrentNodes = nodeCollection;
  33:  
  34:            if (newCurrentNodes.Count > 0)
  35:            {
  36:                pubweb.InheritCurrentNavigation = false;
  37:                pubweb.Update();
  38:            }
  39:            pubweb = PublishingWeb.GetPublishingWeb(web);
  40:  
  41:            // If we've got global or current nodes in the xml then the intent is to reset those elements.
  42:            // If we've also specified to delete any existing elements then we need to first hide all the
  43:            // sub-sites and pages (you can't delete them because they don't exist as a node).  Note that
  44:            // we are only doing this if showSubSites is true - if it's false we don't see them so no point
  45:            // in hiding them.  Any non-sub-site or non-page will be deleted after we've added the new nodes.
  46:            foreach (SPWeb tempWeb in pubweb.Web.Webs)
  47:            {
  48:                try
  49:                {
  50:                    if (newCurrentNodes.Count > 0 && deleteExistingCurrent && showSubSites)
  51:                    {
  52:                        pubweb.ExcludeFromNavigation(false, tempWeb.ID);
  53:                    }
  54:                }
  55:                finally
  56:                {
  57:                    tempWeb.Dispose();
  58:                }
  59:            }
  60:            pubweb.Update();
  61:  
  62:            // Now we need to add all the current nodes (if any)
  63:            AddNodes(pubweb, false, pubweb.CurrentNavigationNodes, newCurrentNodes);
  64:            // Update the web as the above may have made modifications
  65:            pubweb.Update();
  66:  
  67:            // Now delete all the previously existing current nodes.
  68:            if (newCurrentNodes.Count > 0 && deleteExistingCurrent)
  69:            {
  70:                foreach (SPNavigationNode node in existingCurrentNodes)
  71:                {
  72:                    node.Delete();
  73:                }
  74:            }
  75:        }
   1: /// <summary>
   2:        /// Adds the nodes.
   3:        /// </summary>
   4:        /// <param name="pubWeb">The publishing web.</param>
   5:        /// <param name="isGlobal">if set to <c>true</c> [is global].</param>
   6:        /// <param name="existingNodes">The existing nodes.</param>
   7:        /// <param name="newNodes">The new nodes.</param>
   8:        private static void AddNodes(PublishingWeb pubWeb, bool isGlobal, SPNavigationNodeCollection existingNodes, List<NavigationNode> newNodes)
   9:        {
  10:            if (newNodes.Count == 0)
  11:                return;
  12:  
  13:            for (int i = 0; i < newNodes.Count; i++)
  14:            {
  15:                NavigationNode newNodeXml = (NavigationNode)newNodes[i];
  16:                string url = newNodeXml.Url;
  17:                string title = newNodeXml.Title;
  18:                NodeTypes type = newNodeXml.NodeType;
  19:  
  20:                bool isVisible = true;
  21:  
  22:                if (type == NodeTypes.Area)
  23:                {
  24:                    // You can't just add an "Area" node (which represents a sub-site) to the current web if the
  25:                    // url does not correspond with an actual sub-site (the code will appear to work but you won't
  26:                    // see anything when you load the page).  So we need to check and see if the node actually
  27:                    // points to a sub-site - if it does not then change it to "AuthoredLinkToWeb".
  28:                    SPWeb web = null;
  29:                    try
  30:                    {
  31:                        string name = url.Trim('/');
  32:                        if (name.Length != 0 && name.IndexOf("/") > 0)
  33:                        {
  34:                            name = name.Substring(name.LastIndexOf('/') + 1);
  35:                        }
  36:                        try
  37:                        {
  38:                            // pubWeb.Web.Webs[] does not return null if the item doesn't exist - it simply throws an exception (I hate that!)
  39:                            web = pubWeb.Web.Webs[name];
  40:                        }
  41:                        catch (ArgumentException)
  42:                        {
  43:                        }
  44:                        if (web == null || !web.Exists || web.ServerRelativeUrl.ToLower() != url.ToLower())
  45:                        {
  46:                            // The url doesn't correspond with a sub-site for the current web so change the node type.
  47:                            // This is most likely due to copying navigation elements from another site
  48:                            type = NodeTypes.AuthoredLinkToWeb;
  49:                        }
  50:                        else if (web.Exists && web.ServerRelativeUrl.ToLower() == url.ToLower())
  51:                        {
  52:                            // We did find a matching sub-site so now we need to set the visibility
  53:                            if (isVisible)
  54:                                pubWeb.IncludeInNavigation(isGlobal, web.ID);
  55:                            else
  56:                                pubWeb.ExcludeFromNavigation(isGlobal, web.ID);
  57:                        }
  58:                    }
  59:                    finally
  60:                    {
  61:                        if (web != null)
  62:                            web.Dispose();
  63:                    }
  64:  
  65:                }
  66:                else if (type == NodeTypes.Page)
  67:                {
  68:                    // Adding links to pages has the same limitation as sub-sites (Area nodes) so we need to make
  69:                    // sure it actually exists and if it doesn't then change the node type.
  70:                    PublishingPage page = null;
  71:                    try
  72:                    {
  73:                        // Note that GetPublishingPages()[] does not return null if the item doesn't exist - it simply throws an exception (I hate that!)
  74:                        page = pubWeb.GetPublishingPages()[url];
  75:                    }
  76:                    catch (ArgumentException)
  77:                    {
  78:                    }
  79:                    if (page == null)
  80:                    {
  81:                        // The url doesn't correspond with a page for the current web so change the node type.
  82:                        // This is most likely due to copying navigation elements from another site
  83:                        type = NodeTypes.AuthoredLinkToPage;
  84:                        url = pubWeb.Web.Site.MakeFullUrl(url);
  85:                    }
  86:                    else
  87:                    {
  88:                        // We did find a matching page so now we need to set the visibility
  89:                        if (isVisible)
  90:                            pubWeb.IncludeInNavigation(isGlobal, page.ListItem.UniqueId);
  91:                        else
  92:                            pubWeb.ExcludeFromNavigation(isGlobal, page.ListItem.UniqueId);
  93:                    }
  94:                }
  95:  
  96:                // If it's not a sub-site or a page that's part of the current web and it's set to
  97:                // not be visible then just move on to the next (there is no visibility setting for
  98:                // nodes that are not of type Area or Page).
  99:                if (!isVisible && type != NodeTypes.Area && type != NodeTypes.Page)
 100:                    continue;
 101:  
 102:                // Finally, can add the node to the collection.
 103:                SPNavigationNode node = SPNavigationSiteMapNode.CreateSPNavigationNode(
 104:                    title, url, type, existingNodes);
 105:  
 106:  
 107:                // Now we need to set all the other properties
 108:  
 109:                // If we didn't have a CreatedDate or LastModifiedDate then set them to now.
 110:                if (node.Properties["CreatedDate"] == null)
 111:                    node.Properties["CreatedDate"] = DateTime.Now;
 112:                if (node.Properties["LastModifiedDate"] == null)
 113:                    node.Properties["LastModifiedDate"] = DateTime.Now;
 114:  
 115:                // Save our changes to the node.
 116:                node.Update();
 117:                node.MoveToLast(existingNodes); // Should already be at the end but I prefer to make sure :)
 118:  
 119:            }
 120:  
 121:        }
 122:    }


Conclusion

Setting up navigation while provisioning the site in code can be hard. Unfortunately it’s not that easy as the AreaNavigationSettings page (in the screenshot) makes you believe it is.

Took me quite a while to get it working but thanks to guys like Gary I’ve managed it..If you read his comments in the code you can see it was quite the challenge to get this thing right. So thanks again mate! :)

Discussions *BETA* Central

Right! Next to the Blogs Central product (demo at http://demo.zevenseas.com btw), we now also have Discussion Central.. which is my little baby ;)

So, what does it do? The primary function of it is to aggregate discussions from discussion lists throughout all the WebApplications that are associated to the same SSP. And an example of such an aggregation looks like this:

discussionoverview

You may have noticed that it looks a lot like the Live Feed page from Facebook.. and yes.. you are correct! Facebook was my inspiration (quite literally :)

We also added the ViewTracking mechanism, this piece of functionality comes from Blog Central. By adding this type of functionality we can quickly see which discussions are viewed most and thus we can sort on that. Basically the same as any other Web2.0 aggregation page where you can sort on Most Replies/Most Viewed.

Another, more abstract, view of those discussions is the following :

discussionstyle2

So.. let me know your thoughts on this one.. you can play with it yourself real soon on http://demo.zevenseas.com  :)

IGNITE recap

After a pretty exhausting but very interesting week I can say that SharePoint 2010 will be very.. awesome..  (that was pretty much THE word of the week by a particular trainer and a few attendees (amongst them were SharePoint heroes like Tobias Zimmergren, Waldek Mastykarz, Joris Poelmans (AKA JOPX)))

After seeing a lot of sessions in Vegas at the SPC09, I thought that I had seen all the cool new things that are coming but this week showed a couple more.

One of the biggest new things for me was the removal of .stp files and instead of those, now we have .wsp’s that are called “WebTemplates”. Because they are .wsp’s, we can make use of the upgrade functionality. Meaning that in 2010 we have UPGRADEABLE webtemplates!!

Once more..

in 2010 we have UPGRADEABLE webtemplates!!!

To give you the bigger picture ..

  1. User can click together the layout..
  2. User can save the site as template
  3. Developer can import the WSP into VS
  4. Developer can upload the WSP as a farm solution
  5. Sites can be created based on that custom webtemplate
    1. Developer modifies the webtemplate according to new business needs
    2. Developer updated WSP
    3. WSP get’s upgraded
    4. Existing sites get updated using the Feature Update framework

(at least.. that’s the story ;))

Can’t believe they didn’t shout this one out as big as the ‘F5 experience’ for example. I mean.. businesses were (and are) waiting for many years to have this functionality available..

For now I just wanted to say.. thanks Microsoft for making this possible, thanks Wouter van Vugt, Vesa Juvonen and Todd Carter for giving an awesome developer training and thanks Dan for giving me his seat ;)

<< Previous    Next >>

 
 
 

© 2009 Community Kit For SharePoint