Tuesday, September 27, 2011

Branding SharePoint Server 2010 E-mail Alerts

Background
My organization relies heavily on SharePoint discussion boards and the SharePoint discussion board alert system. Our clients use the alerts too. So, I was asked to brand them.

There are a couple of options when branding SharePoint alerts.
  1. Change the XSLT that is used to generate the alert (ugh! I hate XSLT)
  2. Write an OnNotification method for a class that implements IAlertNotifyHandler
I chose option 2 for our organization. This post will show the before and after as well as a high level explanation of the technical details.

Before and After
Here is what an alert for a discussion board looks like out of the box (see annotations for numbered boxes below the image)


Annotations:
  1. "Connect to this Discussion Board" is Outlook functionality that allows the user to monitor posts and updates to SharePoint Lists. It shows read and unread items. Discussions can be replied to without ever going to the SharePoint site, which can be handy if you spend a lot of time in Outlook.
  2. "Modify my alert settings" links the user to their alert settings for the site. It basically takes you to [Site]/_layouts/mysubs.aspx
  3. "View [Item Subject] links the user to the item's view form. If it is a message (message is a SharePoint content type that acts like a list item. A single message is a reply to a discussion post), it takes you to the message's view form. If it is a discussion (discussion is a SharePoint content type that acts like a folder. It contains the original post of a new discussion), it takes you to the discussion's view form.
  4. "View [Discussion Board Name]" takes you to the default view of the discussion board (usually a list of discussions).
Here is what it looked like after I implemented option 2 for our organization (I had to blur a lot out...sorry):


There are a few notable differences between the before and after:
  1. "Connect to this Discussion Board" is gone. We specifically don't want our external users to be able to connect to the discussion board. In fact, we prefer it if they don't know that it is SharePoint.
  2. I added a logo to the top left corner of the e-mail body.
  3. "View [Item Subject]" now says "View or Reply to [Item Subject]" and instead of taking you to the item form it takes you to the default message view for the discussion, which is usually either the threaded or the flat view.
  4. Obviously I change the color scheme a bit to be consistent with our logo and branding and I removed all of the fields except the body and indented the body a bit. I also changed the wording of the title a bit and made it (what I think) is a bit cleaner and more user friendly.
Note that this is a very simple design with logo, link to reply, and color/CSS changes. You obviously have a lot of flexibility to make this look how you want.

Technical Details
I am going to give a high level of the important technical details here, but I will probably have to go into more detail in future blog posts as there is quite a bit under the surface.

You should create a solution package that will put a dll into the GAC. The solution package should contain a class that implements IAlertNotifyHandler and has a method called OnNotification. You should use this method to create the new alert format. It should look something like this:

The beginning of the class looks like this:



The end of the class looks like this:


Notice that I add a linked resource for the logo and use the SmtpClient.Send method to send the mail. All the work to create the build variable that contains the html for the e-mail is in the class.

Once you create your class and deploy it to the GAC, there is more work to do:

Create a copy of this file: Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\XML\alerttemplates.xml. This file contains the alert templates for each of the different lists, document libraries, etc. You can find the schema here: http://msdn.microsoft.com/en-us/library/bb802961.aspx. Note that there is a separate file for SMS. I specifically didn't bother to update that file since we don't use it and it is disabled, but you will need to update that file as well, if you want to change your SMS.

Once you have a copy, you need to add the NotificationHandlerAssembly and NotificationClassName Properties in the newly copied file for alert template you would like to override with your new design. For example, I was changing the discussion board alerts. So, I found the section in the XML that pertained to discussion boards, then I added the appropriate entries in the properties section. You can find an example of the entries here: http://msdn.microsoft.com/en-us/library/ff407215.aspx. These entries should point to your new assembly and class.

Once that is done, you need to tell SharePoint to use the new alerttemplates file that you just created. You do that with this command: http://technet.microsoft.com/en-us/library/dd278299(office.12).aspx

Then iisreset and restart the timer service.

I would be happy to go into more technical details if someone has questions. Just post a comment!

Happy SharePointing!

Thursday, July 28, 2011

Quick and Dirty Evaluation of Lightning Tools Social Squared

Out of the Box discussion boards for SharePoint 2010 are not the shining example of a perfect collaborative user experience. My organization is looking at third party tools or possibly developing something internally to replace them. So...


I did a quick and dirty evaluation of Lightning Tools Social Squared for SharePoint 2010 (http://www.lightningtools.com/social-squared/default.aspx) today and thought I would share my findings in case anyone is interested. I sent this feedback to Lightning Tools and will post any responses if I get any.


Note that this evaluation was specific to my organization's needs. Other organizations may have different needs.

There are a few features that we don't need or wouldn't use:
  • Forum badges and reputation: while I can see the need for this in some situations, we are a fairly small organization where everybody knows each other and badges and reputation would not be appropriate or productive.
  • Display active forum users: we have other communication mechanisms. This would be redundant and possibly counterproductive.
  • Follow a user: We have a full MySites implementation. This seems redundant
  • User post count: not relevant to us
There is at least one feature that we would like that is missing:
  • There appears to be no way to sort forums in any other way than ascending. We would want descending (latest post on top) on all forums.
I am concerned about the integration with SharePoint:
  • What’s the backup and restore story? It appears that everything is installed in a separate database. I am assuming then that normal SharePoint backups wouldn’t backup the Social Squared forums?
  • Search seems to be completely separate. I would want search to be integrated with our existing SharePoint search functionality.
  • I would want tagging to integrate with SharePoint social tagging. It appears to be completely separate.



[Edit 7/29/2011]
As a follow up to my original post, Lightning Tools did provide a resonse to my comments:


1, User Reputation and Badges

This is something that can be turned off within Site Settings -> Forum Settings. If you turn it off the badges will not appear.


2, The active forum users web part is completely optional. As it's a separate web part you can decide whether to use it or not.


3, We do have a Recent Activity View, here is the one for our Connect forums. It shows all topics across the forums that have had activity over the last 7 days:



4, Backup and restore - as Social Squared is using a separate SQL database you would need to create a backup and restore plan for this. You can quite easily setup some scheduled tasks to do this each evening.
 

5, SharePoint Search - if you have the Business Data Catalog or Business Connectivity Services components available in SharePoint Server you can integrate Social Squared with SharePoint Search. In the Social Squared download there's a folder called SharePointSearchIntegration which has documentation and files to accomplish this.


6, Tagging is a separate component right now and does not use the 2010 Term Store. This is something we are currently working on and plan on including it in our next release which is scheduled for September.

Saturday, May 7, 2011

SharePoint 2010 Metadata-based Tag Cloud Web Part

I recently had the opportunity to create a search-based tag cloud web part for SharePoint 2010. This blog post is a walkthrough of the code created to do it. Note that there were specific business requirements that required that the metadata tags be pulled directly from a list. The code could easily be modified, however, to pull the metadata tags from another location. You could, for example, use Linq or search itself to pull the data.

I am going to jump right into the code and provide notes along the way.

Here are the using statements that you will need:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;


Below is the namespace and the class declaration. As with virtually any web part I create, inherit from the WebPart class:

namespace My.WebParts
{
    [ToolboxItemAttribute(true)]
    public class TagCloud : WebPart
    {


I'm going to start by creating two dictionaries. The first dictionary is sorted and will be used to hold the words that will appear in the tag cloud (string key) along with the number of occurences of the word (int). Tag clouds are generally in alphabetical order, which is why I use a sorted dictionary for this one.

The second dictionary stores the ids associated with a tag. These are the actual metadata ids stored in the SharePoint database. I need these because I want, when you click on a tag, to display the search results for that tag. We are going to use a refinement parameter in search (more on that later) and this requires the metadata id.

        private SortedDictionary<string, int> dict = new SortedDictionary<string, int>();
        private Dictionary<string, string> dictTagIds = new Dictionary<string, string>();


Next I will declare the web part properties that can be set by the SharePoint user or administrator that creates the web part.

The first property is the name of the list that contains the metadata column to be used in the tag cloud. I agree that this approach is somewhat limiting, but this was part of my business requirements. Please keep in my that it is a rather simple change to have the data come from a SharePoint Linq or Search itself.

        [WebBrowsable(true),
        Category("Custom Properties"),
        Personalizable(PersonalizationScope.Shared),
        DefaultValue("My List"),
        WebDisplayName("List Name"),
        WebDescription("Name of the list that contains the metadata column to be used in the tag cloud")]
        public string listName { get; set; }


The second column tells the code which column on the list contains the metadata column to be used in the tag cloud. I don't check to make sure it's a metadata column in the code, but you could easily add one to make sure that the user enters a valid metadata column on the list. In fact, a nice enhancement would be to just make it a drop-down that shows valid metadata columns on the list selected:

   [WebBrowsable(true),
   Category("Custom Properties"),
   Personalizable(PersonalizationScope.Shared),
   DefaultValue("Tag"),
   WebDisplayName("Column Name"),
   WebDescription("Name of the metadata colum to be used in the tag cloud")]
   public string fieldName { get; set; }


The third property sets the maximum tags to display in the tag cloud. I don't set an overall limit regardless of what the user enters, but that should probably be there as well:

   [WebBrowsable(true),
   Category("Custom Properties"),
   Personalizable(PersonalizationScope.Shared),
   DefaultValue("50"),
   WebDisplayName("Maximum Tags to Display"),
   WebDescription("Enter the maximum number of tags to display")]
   public int maxNumberOfTags { get; set; }


The fourth property is the search scope to use when you click on a tag and display search results. Again, I don't validate this, but you could easily add that:

   [WebBrowsable(true),
   Category("Custom Properties"),
   Personalizable(PersonalizationScope.Shared),
   DefaultValue("MySearchScope"),
   WebDisplayName("Search Scope"),
   WebDescription("The Search Scope to be used when clicking on a tag in the tag cloud")]
   public string searchScope { get; set; }


The fifth property is the relative url of the search results page to show when you click on a tag. We use a custom search results page for the tag cloud. So, this was necessary:

   [WebBrowsable(true),
   Category("Custom Properties"),
   Personalizable(PersonalizationScope.Shared),
   DefaultValue("../SearchCenter/Pages/Results.aspx"),
   WebDisplayName("Search Center Relative URL"),
   WebDescription("The relative URL of the search results page to use when clicking on a tag in the tag cloud")]
   public string searchCenterResultsPageUrl { get; set; }


The final two properties let the user set the minimum and maximum font sizes to use in the tag cloud:

   [WebBrowsable(true),
   Category("Custom Properties"),
   Personalizable(PersonalizationScope.Shared),
   DefaultValue(10),
   WebDisplayName("Minimum Tag Font Size"),
   WebDescription("The minimum size font to use in the tag cloud")]
   public int minFontSize { get; set; }


   [WebBrowsable(true),
   Category("Custom Properties"),
   Personalizable(PersonalizationScope.Shared),
   DefaultValue(35),
   WebDisplayName("Maximum Tag Font Size"),
   WebDescription("The maximum size font to use in the tag cloud")]
   public int maxFontSize { get; set; }


Override the CreateChildControl method and call the methods that do the grunt work:

   protected override void CreateChildControls()
   {
       GetMetadataValues();
       Controls.Add(new LiteralControl(BuildTagCloudHtml()));
   }


The GetMetadataValues method goes out to the list that was specified in the web part properties and gets the metadata from the appropriate column. It then uses Linq to count each of the occurences of a tag. Once everything is counted and sorted, I trim off the least frequent occurences of a word based on the web part property. And finally, the words along with their occurences are put into the sorted dictionary. If you wanted to change the web part to pull from a search query instead of a list, this is where you would do it.

        private void GetMetadataValues()
        {
            using (SPWeb web = SPContext.Current.Site.RootWeb)
            {
                try
                {
                    SPList queryList = web.Lists[listName];
                    SPListItemCollection items = queryList.GetItems();

                    List<string> words = new List<string>();
                    foreach (SPListItem item in items)
                    {
                        string[] tags = item[fieldName].ToString().Split(';');
                        foreach (string tag in tags)
                        {
                            string[] tagLabelAndGuid = tag.Split('|');
                            string tagLabel = tagLabelAndGuid[0];
                            string tagGuid = tagLabelAndGuid[1];
                            words.Add(tagLabel);
                            if (!dictTagIds.ContainsKey(tagLabel))
                                dictTagIds.Add(tagLabel, tagGuid);
                        }
                    }

                    var wordCount =
                        from word in words
                        group word by word into g
                        select new { g.Key, Count = g.Count() };

                   
                    var trimmedWordCount =
                        (from word in wordCount
                         orderby word.Count descending
                         select word).Take(maxNumberOfTags);

                   
                    foreach (var stuff in trimmedWordCount)
                    {
                        dict.Add(stuff.Key, stuff.Count);
                    }
                }
                catch (Exception ex)
                {
                    if (!EventLog.SourceExists("TagCloudWebPart"))
                        EventLog.CreateEventSource("TagCloudWebpart", "Application");
                    EventLog.WriteEntry("TagCloudWebpart", ex.Message);
                }
            }
        }


Once I have all the data, I build the html that will set the relative font sizes for the tag cloud. Note that, each tag cloud term has a link attached to it. Using the scope and custom search page specified in the web part parameters, I add parameters to the search link that narrow the search results to only include the items tagged with the metadata term that was clicked. This is essentially the "r" parameter (for refinement) in the search results. The r parameter must be url encoded and requires both owstaxid field name and the guid of the field (which is why I retrieved that in the last step).

        private string BuildTagCloudHtml()
        {
            StringBuilder htmlString = new StringBuilder();

            int minVal = FindDictionaryMinValue(dict);
            int maxVal = FindDictionaryMaxValue(dict);

            if (dict.Count > 0)
            {
                htmlString.Append("<p><center>");
                foreach (var word in dict)
                {
                    double weight = (Math.Log(word.Value) - Math.Log(minVal)) / (Math.Log(maxVal) - Math.Log(minVal));
                    int fontsize = (int)(minFontSize + Math.Round((maxFontSize - minFontSize) * weight));
                   
                    string guid;
                    dictTagIds.TryGetValue(word.Key, out guid);
                    string rParameter = "\"owstaxId" + fieldName + "\"=#" + guid + ":\"" + word.Key + "\"";
                    rParameter = "&r=" + System.Web.HttpUtility.UrlEncode(rParameter);
                    string kParameter = "?k=" + word.Key;
                    string sParameter = "&s=" + searchScope;
                    string title = "\" title=\"" + word.Key;
                    string style = "\" style=\"font-size:" + fontsize + "pt\">";

                    htmlString.Append("<a href=\"");
                    htmlString.Append(searchCenterResultsPageUrl);
                    htmlString.Append(kParameter);
                    htmlString.Append(sParameter);
                    htmlString.Append(rParameter);
                    htmlString.Append(title);
                    htmlString.Append(style);
                    htmlString.Append(word.Key);
                    htmlString.Append("</a> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ");
                }
                htmlString.Append("</center></p>");
            }
            else
            {
                htmlString.Append("<p>No Results</p>");
            }

            return htmlString.ToString();
        }


There are two helper methods that calculate the minimum value and the maximum value in the sorted dictionary:

        private int FindDictionaryMaxValue<T, U>(SortedDictionary<T, U> enumerable)
        {
            int maxVal = int.MinValue;

            foreach (KeyValuePair<T, U> pair in enumerable)
            {
                int curVal = Convert.ToInt32(pair.Value);
                if (curVal > maxVal)
                {
                    maxVal = curVal;
                }
            }

            return maxVal;
        }

        private int FindDictionaryMinValue<T, U>(SortedDictionary<T, U> enumerable)
        {
            int minVal = int.MaxValue;

            foreach (KeyValuePair<T, U> pair in enumerable)
            {
                int curVal = Convert.ToInt32(pair.Value);
                if (curVal < minVal)
                {
                    minVal = curVal;
                }
            }

            return minVal;
        }


Finally, close the class and the namespace:

    }
}


A few additional notes:
  • The font sizes for the tag cloud are calculated using a logarithmic method that tries to evenly distribute the font sizes across the collection of tags. You may want to change this depending on your expected data distribution. I think, however, that the formula does a very nice job of evenly distributing things.
  • The metadata column on the list and the metadata field in the store are assumed to have the same name in this code. This was based on the business requirements I was given. You may want to change that dependency if you follow this code exactly.
  • The owstaxId portion of the field name is hard-coded in the r parameter. This is setup automatically for every metadata field created. See this post for details: http://msdn.microsoft.com/en-us/library/ff625182.aspx
  • This code was setup and tested using enterprise search on SharePoint 2010. No other versions were tested with this code.
Enjoy! And please post comments if you have some thoughts about it or suggestions for improvement.

Tuesday, May 3, 2011

Using SharePoint 2010 Search Query String Parameters

SharePoint 2010 Enterprise Search query string parameters are almost exactly the same as they were in SharePoint 2007. However, I would like to highlight them, because they can be useful when directing custom links to a search page. You might want to do this if you are creating a custom search-based tag cloud, for example. You could code the tags in the tag cloud to link to the appropriate search page and include a refinement parameter in your query string (see below for more about the refinement parameter). This Microsoft page highlights most of the query string parameters:
http://msdn.microsoft.com/en-us/library/aa637082.aspx

There is another parameter that is not documented in the link above that is available on SharePoint 2010 Server. The r parameter (for refinement). Thanks Steve Curran for pointing this out here: http://social.msdn.microsoft.com/Forums/en-US/sharepoint2010general/thread/5868ed16-7602-4251-b28f-310b479dd1ec
The refinement parameter is a little bit trickier than the other parameters because (as Steve mentions):
  • It must be URL encoded
  • It requires a k parameter to be present
The refinement parameter also requires that the field you are refining on be setup as a managed metadata property in the SharePoint Search Service.
So let's say you are using a server side code-behind to create a custom link to a search results page. It might look something like this in C# (replace bracketed items with your actual items):

string rParameter = System.Web.HttpUtility.UrlEncode("title=\"[Sample Title]\"");
string url = "[site collection path]/searchcenter/pages/results.aspx?k=[Sample]&r=" + rParameter;

The result is a string that looks like this (again, replace the bracketed information with your data):

http://[site collection path]/searchcenter/pages/results.aspx?k=[Sample]&r=%22title%22%3D%22[Sample Title]%22