Saturday, August 1, 2009

Using the calendar control on a public facing SharePoint Publishing Site

 

The Problem

If you have ever put a SharePoint publishing site on the internet, you know there can be numerous challenges.  One challenge we recently encountered revolved around exposing a SharePoint calendar to the public.  The calendar would show up just fine but when you would hit the site anonymously and click on one of the calendar event, you would receive a login prompt.  We had written a custom calendar list definition, with a custom content type and a custom display form.  Turns out the issue was because we had enabled the ViewFormPagesLockDown feature to prevent anonymous users from browsing to the SharePoint back end (See this post for more information http://technet.microsoft.com/en-us/library/cc263468.aspx).  Enabling the lock down feature disables access to view Application Pages which is what the dispform.aspx file for a calendar definition is.   After much Googling, most of the posts we could find recommended disabling the ViewFormPagesLockDown feature but that was not a viable option for us.  We attempted to change the display form for the calendar using SharePoint Designer but it turns out it was still posting to the dispform.aspx application page and then redirecting to the value we set using designer.  This of course still did not resolve the issue because it still required hitting an application page. 

The Solution

Our solution turned out to be two web parts, one to present the calendar and one to display the detail of the calendar event.

Presenting the calendar

We were actually quite pleased with the presentation of the calendar and only wanted to change the target of the link created for each calendar event to point to a publishing page rather than an application page.  I also did not want to battle how to expand all of the recurring items.  Turns out using the SharePoint object model there is a SPCalendarView object to accomplish the same thing that is accomplished by adding a calendar to a publishing page like a web part.  Essentially, when you do this, you are just putting a calendar view on the page.  So our solution for this piece was to write a web part that would instantiate a SPCalendarView object and build a SPCalendarItemCollection object that binds to the view.  By building the collection of calendar items we are able to manually set the display form url of each calendar item. 

To avoid the problem of manually expanding recurring items, SharePoint offers an object to accomplish this.  The secret is to use a SPQuery object with the query set to the CAML shown in the following code snippet.  In addition, you set the ExpandRecurrence property to true.  This automatically expands the recurrence in to all the appropriate individual events for you to add to your calendar item collection.

using System;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Serialization;

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.WebPartPages;
using System.ComponentModel;
using System.Xml;
using System.Xml.Xsl;
using System.Net;
using System.Xml.XPath;
using System.Text;
using System.IO;
using System.Web;

namespace CalendarPresentation
{

    [Guid("b6e3affc-9d86-41ce-b323-cd74164947f3")]
    public class CalendarPresentation : System.Web.UI.WebControls.WebParts.WebPart
    {

        [Personalizable(PersonalizationScope.Shared),
        WebBrowsable(true),
        WebDisplayName("Calendar URL"),
        WebDescription("Provide the url for the target calendar. This is only needed if the calendar is in another site."),
        Category("Calendar Presentation Settings")]
        public string calendarURL { get; set; }

        [Personalizable(PersonalizationScope.Shared),
        WebBrowsable(true),
        WebDisplayName("Calendar Name"),
        WebDescription("Provide Name of the target calendar"),
        Category("Calendar Presentation Settings")]
        public string calendarName { get; set; }

        [Personalizable(PersonalizationScope.Shared),
        WebBrowsable(true),
        WebDisplayName("Display Form URL"),
        WebDescription("Provide Name URL to the display form."),
        Category("Calendar Presentation Settings")]
        public string dispFormURL { get; set; }

        XmlDocument doc = new XmlDocument();
        Literal lit = new Literal();
        SPCalendarItemCollection calItemCollection = new SPCalendarItemCollection();
        SPListItemCollection calBase = null;

        public CalendarPresentation()
        {
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);

            if (!String.IsNullOrEmpty(calendarName) && !String.IsNullOrEmpty(dispFormURL))
            {
                try
                {
                    //We need to run with elevated permissions if the 
                    //document library containing
                    //the transform does not have anonmymous read access.


                    /*  The following block sets up the SharePoint Site and Web depending on
                     * a calendarURL was specified or not.  If no calendar URL was specified
                     * the current SPContext site and web are used.  If a calendar URL was specified
                     * that URL is used to determine the site and web.  This allows the web part
                     * to present a calendar across sites.
                     */
                    SPSite siteColl = null;
                    SPWeb site = null;
                    if (string.IsNullOrEmpty(calendarURL))
                    {
                        siteColl = SPContext.Current.Site;
                        site = SPContext.Current.Web;
                    }
                    else
                    {
                        siteColl = new SPSite(calendarURL);
                        site = siteColl.OpenWeb();
                    }

                    /*Now that we have our site and web we need to instantiate a new instance
                     * with elevated permissions.  This is what allows calendars to be accessed
                     * from a different site than where they reside.
                     */
                    SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                    {
                        using (SPSite ElevatedsiteColl = new SPSite(siteColl.ID))
                        {
                            using (SPWeb ElevatedSite = ElevatedsiteColl.OpenWeb(site.ID))
                            {
                                /* Now that we have our context, we need to query the calendar list
                                 * and get the events.  This is where we create a SPQuery object that expands
                                 * all of the recurrences.  You will also see where we set a default CalendarDate
                                 * and update it if there is a CalendarDate specified in the querystring.
                                 * This is to accommodate the existing calendar view functionality that allows
                                 * you to changes months/days etc.
                                 */
                                SPQuery query = new SPQuery();
                                query.Query = "<Where><DateRangesOverlap><FieldRef Name=\"EventDate\" /><FieldRef Name=\"EndDate\" /><FieldRef Name=\"RecurrenceID\" /><Value Type=\"DateTime\"><Month /></Value></DateRangesOverlap></Where>";
                                query.ExpandRecurrence = true;
                                DateTime calDate = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
                                DateTime.TryParse(HttpContext.Current.Request.QueryString["CalendarDate"], out calDate);
                                query.CalendarDate = calDate;
                               
                                /*The following line actually queries the calendar with the SPquery object
                                 * created above.  This returns a SPListItemCollection.
                                 */
                                calBase = ElevatedSite.Lists[calendarName].GetItems(query);
                               
                                /*Now we are going to iterate the SPListItemCollection and create a 
                                 * SPCalendarItemCollection.
                                 */
                                foreach (SPListItem item in calBase)
                                {
                                    //Instantiage a calendar item for each iteration.
                                    SPCalendarItem calItem = new SPCalendarItem();
                                    
                                    /* Some of the information required to create a calendar item are found
                                     * in the SPListItem XML property.  We need to get this information into
                                     * a XPathNavigator so we can easily retrieve it.
                                     */
                                    XmlDocument doc = new XmlDocument();
                                    doc.LoadXml(item.Xml);
                                    XPathNavigator xDoc = doc.CreateNavigator();
                                    xDoc.MoveToRoot();
                                    xDoc.MoveToFirstChild();

                                    //Start Building the calendar item.
                                    calItem.ItemID = item.ID.ToString();
                                    // Here is an example of how to get the info out of the XPathNavigator.
                                    calItem.Title = xDoc.GetAttribute("ows_Title", "");  
                                    calItem.Location = xDoc.GetAttribute("ows_Location", "");

                                    DateTime myStartDate = DateTime.MinValue;
                                    DateTime.TryParse(xDoc.GetAttribute("ows_EventDate", ""), out myStartDate);
                                    calItem.StartDate = myStartDate;

                                    DateTime myEndDate = DateTime.MinValue;
                                    DateTime.TryParse(xDoc.GetAttribute("ows_EndDate", ""), out myEndDate);
                                    calItem.EndDate = myEndDate;

                                    if (calItem.EndDate != DateTime.MinValue)
                                        calItem.hasEndDate = true;
                                    else
                                        calItem.hasEndDate = false;

                                    int myIsAllDayItem = 0;
                                    int.TryParse(xDoc.GetAttribute("ows_fAllDayEvent", ""), out myIsAllDayItem);
                                    calItem.IsAllDayEvent = Convert.ToBoolean(myIsAllDayItem);

                                    int myIsRecurrence = 0;
                                    int.TryParse(xDoc.GetAttribute("ows_fRecurrence", ""), out myIsRecurrence);
                                    calItem.IsRecurrence = Convert.ToBoolean(myIsRecurrence);

                                    /* The following line is the ENTIRE reason for jumping through all these hoops.
                                     * We are assigning the displayformurl which is where the link created for each
                                     * calendar event will be targeted.  The dispFormURL variable is defined at the 
                                     * time the web part is added to the page, allowing you to point a calendar view
                                     * to any presentation page you want.  In addition we are putting the ListID and 
                                     * the item id (ID) in the querystring.  This is for the detail display web part 
                                     * to use to query the appropriate list and find the appropriate item.  One very
                                     * important not is the '&' included at the end of the query string we built.  This
                                     * is a bit of a hack because the SharePoint calendar view automatically appends the 
                                     * item ID and the source to the querystring using javascript.  By putting the '&' at
                                     * the end of the string, all of our query string values will be returned as we expect
                                     * when we pull them using the detail display form.  I know this sounds wonky but
                                     * you can try it with and without and see what I mean.
                                     */
                                    calItem.DisplayFormUrl = String.Format("{0}?ListID={1}&ID={2}&", dispFormURL, item.ParentList.ID, item.ID);

                                    //Add the created calendar item to the collection and do it again.
                                    calItemCollection.Add(calItem);
                                    
                                }
                            }
                        }
                    }));

                }
                catch (Exception ex)
                {
                    //TODO: Something went wrong, log something for troubleshooting purposes.
                }
            }
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            //This is where we instantiate the calendar view.
            SPCalendarView calview = new SPCalendarView();
            calview.ListName = calendarName;

            /* This line determines what 'view' the calendar is displayed as.  We want
             * the month view to be the default.  If the user clicks on 'day' for example
             * the CalendarPeriod value is added to the query string and we use that instead.
             */
            calview.ViewType = "month";
            if (!String.IsNullOrEmpty(HttpContext.Current.Request.QueryString["CalendarPeriod"]))
            {
                calview.ViewType = HttpContext.Current.Request.QueryString["CalendarPeriod"];
            }
            
            //Set the calendar view datasource to the collection we just created.
            calview.DataSource = calItemCollection;
            //Bind it.
            calview.DataBind();

            /*Add the view to the page, unless we haven't configured the web part correctly.
            * If it hasn't been configured properly then we add a message to remind the content 
            * editor to do so.
            */
            if (!String.IsNullOrEmpty(calendarName))
            {
                base.Controls.Add(calview);
            }
            else
            {
                lit.Text = "The Calendar Presentation web part is not properly configured.";
                base.Controls.Add(lit);
            }
        }

    }
}

Showing item detail

To display the detail of an item when we clicked on it required another web part.  This web part was considerably simpler because all it had to do was query the list indicated in the query string for the item indicated in the query string and display the value of the description field.  I might add that the description field was a RichHTMLPublishing field that we added to our custom calendar content type.  We disabled the default description field for our custom calendar.

using System;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Serialization;

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.WebPartPages;
using System.Web;
using System.ComponentModel;

namespace CalendarPresentation
{
    [Guid("5297e36f-0de9-417a-892a-e970b68b2d40")]
    public class CalendarDisplayForm : System.Web.UI.WebControls.WebParts.WebPart
    {

        [Personalizable(PersonalizationScope.Shared),
        WebBrowsable(true),
        WebDisplayName("Item Not Found Text"),
        WebDescription("Provide text to be displayed when item is not found."),
        Category("Calendar Detail Presentation Settings")]
        public string itemNotFoundText { get; set; }

        public CalendarDisplayForm()
        {
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            try
            {
                /* Get the values from the query string that we built using the Calendar
                 * Presentation web part.
                 */
                string listID = HttpContext.Current.Request.QueryString["ListID"];
                string returnURL = HttpContext.Current.Request.QueryString["Source"];
                string itemID = HttpContext.Current.Request.QueryString["ID"];
                
                Literal lit = new Literal();

                // Get the site and web using the url passed by the presentation web part.
                SPSite siteColl = new SPSite(returnURL);
                SPWeb site = siteColl.OpenWeb();

                /* Create a new instance of the site and web using elevated permissions in the 
                 * event we are accessing a calendar on another site.
                 */
                SPSecurity.RunWithElevatedPrivileges(new SPSecurity.CodeToRunElevated(delegate()
                {
                    using (SPSite ElevatedsiteColl = new SPSite(siteColl.ID))
                    {
                        using (SPWeb ElevatedSite = ElevatedsiteColl.OpenWeb(site.ID))
                        {
                            /*Get the list item from the calendar using the listID and itemID passed by the
                             * presentation web part.
                             */
                            SPListItem listItem = ElevatedSite.Lists.GetList(new Guid(listID), false).GetItemById(Convert.ToInt32(itemID));
                            //Get the value of the description field.
                            lit.Text = getFieldValue(listItem, "Description");
                        }
                    }
                }));

                //Add the value of the description field to the page if it is not null.
                if (String.IsNullOrEmpty(lit.Text))
                {
                    lit.Text = itemNotFoundText;
                }

                /* This section adds a button to the page that has a post backurl of the return url
                 * passed by the presentation form.  This allows the user to get back to the calendar.
                 */
                base.Controls.Add(lit);
                base.Controls.Add(new LiteralControl("<br/>"));
                Button btnReturn = new Button();
                btnReturn.PostBackUrl = returnURL;
                btnReturn.Text = "Return to Calendar";
                base.Controls.Add(btnReturn);
            }
            catch (Exception ex)
            {
                
            }            
        }

        // This helper method gets the value from any list item field.
        public static string getFieldValue(SPListItem listItem, string fieldName)
        {
            string text = string.Empty;
            if (fieldName == string.Empty)
            {
                return text;
            }
            try
            {
                object myObj = listItem[fieldName];
                return ((myObj != null) ? myObj.ToString() : string.Empty);
            }
            catch
            {
                return string.Empty;
            }
        } 
    }
}

Conclusion

I hope this post can help point some of you in the right direction for your public facing SharePoint publishing sites.  Even though it is a long way around a problem, I think it ultimately results in a more secure site because it allows you to keep the lock down feature activated and still use the calendar control.  I look forward to your comments.

1 comment:

  1. This is exactly what I was looking for. Do you have a .wsp file for this?

    ReplyDelete