An Alternative Archives Web Part to Solve the Pre-dated Posts Provisioning in the OOTB Blog Site Archives Web Part

imageThe OOB SharePoint 2010 site templates are very useful in different scenarios. The guys at Microsoft were very generous to spare some time and build a site template for blogs. After provisioning the site, I was so happy with it! but then stumbled on the fact that the Archives Web Part did not generate links for my old posts, apparently by design - after doing a decent search over forums - pre dated posts were not provisioned by the Blog Site Template Archives Web Part. I had to remove the Web Part and place a link to the All Posts view on the top link bar and called it Archives. On the long run, it did not sound like a good alternative, I had to have that Web Part.

  

I spent couple of hours today to code the Archives Web Part, and I can’t express how happy I am with it. The approach was very simple: I had to get all published posts; find out in which years the posts were authored; find out in which months the posts were authored, and finally push all this to a Tree View control.

After populating the Tree View nodes, I had to handle the SelectedNodeChanged event to redirect to the Date.aspx page and pass the correct StartDateTime and EndDateTime query string parameters. The query string structure is fairly easy, you needed to supply the time span parameters and the title. For example, to get all the posts in December 2011 the URL should look like this: https://www.sharepointstack.com/Lists/Posts/Date.aspx?StartDateTime=2011-12-01T00:00:00Z&EndDateTime=2012-01-01T02:00:00Z&LMY=December,%202011. Notice that the start and end times are passed in ISO8601 formats.

Web Part Download (WSP)

If you are not a technical blogger and just want to download the Web Part, click here. You don’t have to complete this post. On the other hand, if you are a SharePoint developer like me, get your fingers cracking for some code.

Web Part Development

I created a Visual Web Part. The Web Part had 3 controls:

    1: <SharePoint:CssRegistration 
    2:     Name="<% $SPUrl:~sitecollection/_layouts/styles/SharePointStack/SPBlogTemplate/SPBlogTemplate.css %>" 
    3:     After="CoreV4.css" runat="server"></SharePoint:CssRegistration>
    4: <div id="SPBlogContainer">
    5:     <div id="ArchiveSummaryTitle"><a id="ViewArchive" title="Click to view Archive" runat="server">Archives</a></div>
    6:     <asp:TreeView ID="ArchiveSummaryTree" runat="server" ExpandDepth="0" 
    7:         NodeIndent="0" onselectednodechanged="ArchiveSummaryTree_SelectedNodeChanged" ></asp:TreeView>
    8:     <asp:Literal ID="ArchiveSummaryErrors" runat="server"></asp:Literal>
    9: </div>

The markup is straight forward: I place a title, Tree View, and a Literal. I had to apply some CSS formatting to make sure that the Web Part looks like a SharePoint control. I created a CSS file and placed it under the STYLES mapped folder. The CSS helped me adjust the padding and text formatting.

    1: #ArchiveSummaryTitle
    2: {
    3:     color: #0072bc;
    4:     font-size: 1.2em;
    5:     font-weight: normal;
    6: }
    7:  
    8: #SPBlogContainer
    9: {
   10:     padding-left: 11px;
   11: }

Before moving to the code behind, I needed to create a class to help handle the posts using a Generic List. More details on this later on.

    1: namespace SharePointStack.SPBlogTemplate
    2: {
    3:     class SPBlogPost
    4:     {        
    5:         public SPBlogPost(string title, string month, string year)
    6:         {
    7:             postTitle = title;
    8:             publishingMonth = month;
    9:             publishingYear = year;
   10:         }
   11:  
   12:         private string postTitle;
   13:  
   14:         public string PostTitle
   15:         {
   16:             get { return postTitle; }
   17:             set { postTitle = value; }
   18:         }
   19:  
   20:         private string publishingMonth;
   21:  
   22:         public string PublishingMonth
   23:         {
   24:             get { return publishingMonth; }
   25:             set { publishingMonth = value; }
   26:         }
   27:  
   28:         private string publishingYear;
   29:  
   30:         public string PublishingYear
   31:         {
   32:             get { return publishingYear; }
   33:             set { publishingYear = value; }
   34:         }
   35:     }
   36: }

Now let’s get some Code Behind love Smile I commented wherever needed to explain the code as much as possible.

    1: using System;
    2: using System.Collections.Generic;
    3: using System.Linq;
    4: using System.Web.Caching;
    5: using System.Web.UI;
    6: using System.Web.UI.WebControls;
    7: using Microsoft.SharePoint;
    8: using Microsoft.SharePoint.Utilities;
    9:  
   10: namespace SharePointStack.SPBlogTemplate.ArchiveSummary
   11: {
   12:     public partial class ArchiveSummaryUserControl : UserControl
   13:     {
   14:         static object _lock =  new object();
   15:         List<SPBlogPost> postsBuffer = new List<SPBlogPost>();
   16:         List<int> years = new List<int>();
   17:         string[] months = { "January", "February", "March", "April", "May", "June", "July", "August", 
   18:                               "September", "October", "November", "December" };
   19:         bool duplicates;
   20:  
   21:         protected void Page_Load(object sender, EventArgs e)
   22:         {
   23:             if (!Page.IsPostBack)
   24:             {
   25:                 try
   26:                 {
   27:                     //I chose "using" to automatically dispose SharePoint objects
   28:                     using (SPSite blogSiteCollection = SPContext.Current.Site)
   29:                     {
   30:                         using (SPWeb blogWeb = blogSiteCollection.OpenWeb(SPContext.Current.Web.ServerRelativeUrl))
   31:                         {
   32:                             SPList blogPosts = blogWeb.Lists["Posts"];
   33:  
   34:                             //Set the header link
   35:                             ViewArchive.HRef = blogPosts.DefaultViewUrl;
   36:  
   37:                             SPQuery queryPosts = new SPQuery();
   38:                             
   39:                             //CAML Query that returns only published posts and orders them by date
   40:                             queryPosts.Query = "<OrderBy><FieldRef Name='PublishedDate'/></OrderBy>" + 
   41:                                                 "<Where><Eq><FieldRef Name='_ModerationStatus' />" + 
   42:                                                 "<Value Type='ModStat'>0</Value></Eq></Where>";
   43:                             
   44:                             //Pull the query results directly from the Cache, we don't want to query
   45:                             //SharePoint everytime the Archives Summary Web Part runs. This practice increases 
   46:                             //the performance and comes very handy if the user is an active blogger.
   47:                             SPListItemCollection publishedPosts = (SPListItemCollection)Cache["PublishedPosts"];
   48:                             
   49:                             //If the query results are not avilable in the Cache, then we need to execute the query and
   50:                             //store the results in the Cache for the next request.
   51:                             //The Cache is set with Sliding Expiration of 1 day.
   52:                             if (publishedPosts == null)
   53:                             {
   54:                                 //Since SPWeb is not thread safe, we need to place a lock.
   55:                                 lock (_lock)
   56:                                 {
   57:                                     //Ensure that the data was not loaded by a concurrent thread while waiting for lock.
   58:                                     publishedPosts = blogPosts.GetItems(queryPosts);
   59:                                     Cache.Add("PublishedPosts", publishedPosts, null, Cache.NoAbsoluteExpiration, 
   60:                                         TimeSpan.FromDays(1), CacheItemPriority.High, null);
   61:                                 }
   62:                             }
   63:  
   64:                             //Load all published posts into the postsBuffer. The query results will not be available
   65:                             //outside the "using" block.
   66:                             foreach (SPListItem post in publishedPosts)
   67:                                 postsBuffer.Add(new SPBlogPost(post["Title"].ToString(), 
   68:                                     DateTime.Parse(post["PublishedDate"].ToString()).Month.ToString(), 
   69:                                     DateTime.Parse(post["PublishedDate"].ToString()).Year.ToString()));
   70:                         }
   71:                     }
   72:  
   73:                     //Provision years
   74:                     foreach (SPBlogPost post in postsBuffer)
   75:                         years.Add(int.Parse(post.PublishingYear));
   76:  
   77:                     //Make sure we only have distinct years that are sorted in descending order
   78:                     var yearsList = years.Distinct().ToList();
   79:                     yearsList.Sort();
   80:                     yearsList.Reverse();
   81:  
   82:                     //Add the years to the Tree View
   83:                     foreach (int year in yearsList)
   84:                         ArchiveSummaryTree.Nodes.Add(new TreeNode(year.ToString()));
   85:  
   86:                     //Find out which months have posts in each year and add them to the Tree View as ChildNodes
   87:                     foreach (TreeNode year in ArchiveSummaryTree.Nodes)
   88:                     {
   89:                         for (int i = 12; i >= 1; i--)
   90:                         {
   91:                             foreach (SPBlogPost post in postsBuffer)
   92:                             {
   93:                                 if (post.PublishingMonth == i.ToString() && post.PublishingYear == year.Text)
   94:                                 {
   95:                                     duplicates = false;
   96:                                     foreach (TreeNode item in year.ChildNodes)
   97:                                     {
   98:                                         //Check for any duplicate month entries.
   99:                                         //This will become an issue if the user posts more than one post in a month time.
  100:                                         if (item.Text.ToLower() == months[int.Parse(post.PublishingMonth) - 1].ToLower())
  101:                                             duplicates = true;
  102:                                     }
  103:                                     if (!duplicates)
  104:                                         year.ChildNodes.Add(new TreeNode(months[int.Parse(post.PublishingMonth) - 1], 
  105:                                             post.PublishingMonth, null, null, null));
  106:                                 }
  107:                             }
  108:                         }
  109:                     }
  110:  
  111:                     //Expand the latest year node
  112:                     if (ArchiveSummaryTree.Nodes[0] != null)
  113:                         ArchiveSummaryTree.Nodes[0].Expand();
  114:                 }
  115:                 catch (Exception ex)
  116:                 {
  117:                     //Print the error messege to the user using a Literal
  118:                     ArchiveSummaryErrors.Text = "<img src='" + SPContext.Current.Site + 
  119:                         "_layouts/images/SharePointStack/SPBlogTemplate/error.png' alt='Error' style='display: block;' />" + 
  120:                         "&nbsp;<font color='#ff0000' size='12'>" + ex.Message + "</font>";
  121:                 }
  122:                 finally
  123:                 {
  124:                     //Objects will be disposed during the next Garbage Collector run
  125:                     postsBuffer = null;
  126:                     years = null;
  127:                 }
  128:             }
  129:         }
  130:  
  131:         protected void ArchiveSummaryTree_SelectedNodeChanged(object sender, EventArgs e)
  132:         {
  133:             TreeView summaryLinks = (TreeView)sender;
  134:  
  135:             //Make sure that this is a month node, we don't want to handle a year node selection change
  136:             if (summaryLinks.SelectedNode.Parent != null)
  137:             {
  138:                 string month = string.Empty, year = string.Empty;
  139:                 month = summaryLinks.SelectedNode.Value;
  140:                 year = summaryLinks.SelectedNode.Parent.Value;
  141:  
  142:                 //Build the date strings to be converted later on to ISO8601 dates
  143:                 string startDate = month + "/1/" + year;
  144:                 string endDate = startDate;
  145:  
  146:                 //Convert the dates and set the range to be a one month time span
  147:                 startDate = SPUtility.CreateISO8601DateTimeFromSystemDateTime(DateTime.Parse(startDate));
  148:                 endDate = SPUtility.CreateISO8601DateTimeFromSystemDateTime(DateTime.Parse(startDate).AddMonths(1));
  149:  
  150:                 //Build the URL and set the Query String parameters for redirection to the Date.aspx page
  151:                 string url = SPContext.Current.Site + SPContext.Current.Web.ServerRelativeUrl + 
  152:                     "/Lists/Posts/Date.aspx?StartDateTime=" + startDate + "&EndDateTime=" + endDate + 
  153:                     "&LMY=" + months[int.Parse(month) - 1] + ", " + year;
  154:  
  155:                 summaryLinks.Dispose();
  156:  
  157:                 //SharePoint will add "SPSite Url=" to the URL we built. I used Substring to remove it.
  158:                 Response.Redirect(url.Substring(11));
  159:             }
  160:         }
  161:     }
  162: }

Web Part Source Code Download

Well that’s about it Smile If you want the source code you can download it here.

Please don’t hesitate to contact me if you have any questions.