Click here to Skip to main content
15,893,594 members
Articles / Programming Languages / XML

SharePoint Reservations

Rate me:
Please Sign up or sign in to vote.
4.93/5 (15 votes)
29 Mar 2009CPOL8 min read 422.5K   4K   47  
A SharePoint calendar extension that prohibits overlapping appointments
using System;
using System.Collections;
using System.Data;
using System.Security.Permissions;
using System.Runtime.InteropServices;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Security;
using SPDevtools;


namespace ReservationContentType
{
    /// <summary>
    /// Event receiver for Reservations.  Sets the Cancel property when collisions detected
    /// J. Finn Sep 2008
    /// james.finn@gmail.com
    /// </summary>
   
    [CLSCompliant(false)]
    [TargetContentType("f08627ee-d847-49e9-a57e-9e0bc36d8765", "0x010200c33aa3b1e003474c85d76e13291683d5")]
    [Guid("36a65e62-ef0d-4434-aa49-4ad5c68e1579")]
    public class ReservationContentTypeItemEventReceiver : SPItemEventReceiver
    {
        const int MAX_DUPS = 10; // stop looking after this many collisions
        const string RESOURCE_COLUMN = "Resource"; // if need to change the column name
        bool ContainsResource = false; // does the list contains a field named Resource
                                       // which allows a single reservation list to schedule
                                       // multiple reservations independently.

        string ResourceDisplay = String.Empty;

        /// <summary>
        /// Initializes a new instance of the Microsoft.SharePoint.SPItemEventReceiver class.
        /// </summary>
        public ReservationContentTypeItemEventReceiver()
        {
        }

        #region Public Methods
        /// <summary>
        /// Synchronous before event that occurs when a new item is added to its containing object.
        /// </summary>
        /// <param name="properties">
        /// A Microsoft.SharePoint.SPItemEventProperties object that represents properties of the event handler.
        /// </param>
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
        public override void ItemAdding(SPItemEventProperties properties)
        {
            TimeConflict(properties);
        }
        /// <summary>
        /// Synchronous before event that occurs when an existing item is changed, for example, when the user changes data in one or more fields.
        /// </summary>
        /// <param name="properties">
        /// A Microsoft.SharePoint.SPItemEventProperties object that represents properties of the event handler.
        /// </param>
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)]
        public override void ItemUpdating(SPItemEventProperties properties)
        {
            // when using sharepoint designer, page comes in with:
            // EventType=ItemUpdating
            // ListItemId=0
            // only check when ListItemId is non-zero or cannot save page changes
            if (properties.ListItemId != 0)
            {
                TimeConflict(properties);
            }
        }

        #endregion


        

        #region Protected Methods

        /// <summary>
        /// Main method called from ItemAdding and ItemUpdating. Gathers information from properties
        /// to determine the list as well as start and end dates.  After some date validation, queries the
        /// list to determine if any collisions will occur.  If anything fails, then sets the properties.Cancel 
        /// to true and updates properties.ErrorMessage.
        /// </summary>
        /// <param name="properties"></param>
        protected void TimeConflict(SPItemEventProperties properties)
        {
            
            string errorMsg = "";
            try
            {
                SPList reservationList = properties.OpenWeb().Lists[properties.ListId];

                ContainsResource = reservationList.Fields.ContainsField(RESOURCE_COLUMN);
                string oriSelectedResource = String.Empty;
                string newSelectedResource = String.Empty;
                
                if (ContainsResource)
                {
                    newSelectedResource = properties.AfterProperties[RESOURCE_COLUMN].ToString();
                }

                // AfterProperties are null if just approving an event
                string recurData = String.Empty;
                if (properties.AfterProperties["RecurrenceData"] != null)
                {
                    recurData = properties.AfterProperties["RecurrenceData"].ToString();
                }

                // some users reported that recurData not always valid XML
                // when editing an individual reservation from a series.
                // ValidateRecurrence will reset recurData to String.Empty if 
                // invalid XML
                if (recurData != String.Empty)
                {
                    ValidateRecurrence(ref recurData);
                }

                DateTime startDate = DateTime.MaxValue;
                if (properties.AfterProperties["EventDate"] != null)
                {
                    startDate = SPUtility.CreateDateTimeFromISO8601DateTimeString(
                                 properties.AfterProperties["EventDate"].ToString());
                }

                DateTime endDate = DateTime.MaxValue;
                if (properties.AfterProperties["EndDate"] != null)
                {
                    endDate = SPUtility.CreateDateTimeFromISO8601DateTimeString(
                                properties.AfterProperties["EndDate"].ToString());
                }

                bool checkDates = false;
                if (properties.ListItemId == 0) // new one always check the dates
                {
                    checkDates = true;
                }
                else
                {
                    // did the dates/recurrence change?
                    DateTime originalStartDate = DateTime.MaxValue;
                    DateTime originalEndDate = DateTime.MaxValue;
                    string originalRecurData = String.Empty;

                    
                    SPListItem item = properties.ListItem;
                    if (item[item.Fields.GetFieldByInternalName("EventDate").Id] != null)
                    {
                        originalStartDate = (DateTime)item[item.Fields.GetFieldByInternalName("EventDate").Id];
                    }

                    if (item[item.Fields.GetFieldByInternalName("EndDate").Id] != null)
                    {
                        originalEndDate = (DateTime)item[item.Fields.GetFieldByInternalName("EndDate").Id];
                    }

                    if (item[item.Fields.GetFieldByInternalName("RecurrenceData").Id] != null)
                    {
                        originalRecurData = item[item.Fields.GetFieldByInternalName("RecurrenceData").Id].ToString();
                    }

                    if (ContainsResource)
                    {
                        if (item[item.Fields.GetFieldByInternalName(RESOURCE_COLUMN).Id] != null)
                        {
                            oriSelectedResource = item[item.Fields.GetFieldByInternalName(RESOURCE_COLUMN).Id].ToString();
                            oriSelectedResource = oriSelectedResource.Substring(0, oriSelectedResource.IndexOf(";"));
                        }

                    }
                   

                    if ((originalStartDate == DateTime.MaxValue) || (originalEndDate == DateTime.MaxValue))
                    {
                        checkDates = true;
                    }
                    else
                    {
                        if (!((startDate == originalStartDate) && (endDate == originalEndDate) &&
                             (recurData == originalRecurData)))
                        {
                            checkDates = true;
                        }

                        // when approving, do not get the start/end dates provided so need to update them
                        if (startDate == DateTime.MaxValue)
                        {
                            startDate = originalStartDate;
                        }
                        if (endDate == DateTime.MaxValue)
                        {
                            endDate = originalEndDate;
                        }

                    }
                }
                if (!checkDates)
                {
                    if (ContainsResource)
                    {
                        if (oriSelectedResource != newSelectedResource)
                        {
                            checkDates = true;
                        }
                    }
                }

                bool checkOverlap = checkDates;
                // check startDate < endDate
                if (checkDates)
                {
                    if (startDate >= endDate)
                    {
                        checkOverlap = false;
                        errorMsg = "End Date Must be Later than Start Date";
                    }
                }


                if (checkOverlap)
                {
                    DataTable overlapList = new DataTable();

                    // overlapCheckOK = true, no overlaps were found
                    bool overlapCheckOK = FindOverlaps(reservationList, startDate, endDate, newSelectedResource,
                            properties.ListItemId, recurData, ref overlapList, ref errorMsg);

                    if (overlapCheckOK)
                    {
                        if (overlapList.Rows.Count > 0)
                        {
                            errorMsg = formatErrorTable(overlapList);
                        }
                    }
                    else
                    {
                        if (errorMsg != "")
                        {
                            errorMsg = "An Error Occurred";
                        }
                    }
                }

                if (errorMsg != "")
                {
                    properties.ErrorMessage = errorMsg;
                    properties.Cancel = true;
                }

            }
            catch (Exception ex) // general exception handler
            {
                properties.ErrorMessage = "Exception: " + ex.Message + "<BR>" + errorMsg;
                properties.Cancel = true;
            }


        }

        protected bool FindOverlaps(SPList theList, DateTime startDate, DateTime endDate, string SelectedResource,
               int ItemID, string recurData, ref DataTable overlapList, ref string ErrorText)
        {
            bool returnValue = true;

            // DataTable just an easy way to keep track of overlaps
            overlapList = new DataTable();
            overlapList.Columns.Add("Title");
            overlapList.Columns.Add("Start", DateTime.Now.GetType());
            overlapList.Columns.Add("End", DateTime.Now.GetType());
            overlapList.Columns.Add("Modified By");

            bool outerLoop = true; // loop controller

            // All recurring appointments handled by RecurringClasses.Recuring
            // recurring.getNext -- initial call sets up handler
            // returns true if there are more dates to process

            RecurringClasses.Recuring recuring = null;
            if (recurData != String.Empty)
            {
                recuring = new RecurringClasses.Recuring(startDate, endDate, recurData);
                outerLoop = recuring.GetNext(ref  startDate, ref  endDate);
            }

            SPQuery query = null;

            bool moreToDo = true; // inner loop handler
            bool multiplePass = false; // handles range overlapping month end
            int passNumber = 0; 
           

            while (outerLoop)
            {
                moreToDo = true; // inner loop control
                while (moreToDo)
                {
                    // populates the query and sets flags to continue processing
                    moreToDo = populateQuery(startDate, endDate, SelectedResource, ref multiplePass,
                                        ref passNumber, ref query);
                    
                    if (moreToDo)
                    {
                        AddQueryResultsToTable(theList, startDate, endDate, ItemID, SelectedResource, query, ref overlapList, ref ErrorText);
                        if (overlapList.Rows.Count > MAX_DUPS)
                        {
                            // have enough overlapping events to fill the ErrorMessage
                            moreToDo = false;
                            outerLoop = false;
                        }
                        else
                        {
                            if (multiplePass)
                            {
                                passNumber++;
                            }
                            else
                            {
                                moreToDo = false;
                            }
                        }
                    }
                }
                if (outerLoop)
                {
                    if (recuring != null)
                    {
                        // search for the next recurring date range
                        outerLoop = recuring.GetNext(ref  startDate, ref  endDate);
                    }
                    else
                    {
                        outerLoop = false;  // finished if there are no more recurring appointments
                    }
                }
            }


            return (returnValue);
        }

        /// <summary>
        /// Populates an SPQuery object passed by reference that will be used to query the current list.  
        /// Query may return excess values due to the limits of using DateOverlaps query
        /// Since DateOverlaps is limited to specifying week or month, it checks if the start/end dates are
        /// not the same day and query will search for all items for a month
        /// </summary>
        /// <param name="startDate">Start Date</param>
        /// <param name="endDate">End Date</param>
        /// <param name="multiplePass">Are multiple passes necessary to query this date range (ref)</param>
        /// <param name="passNumber">What pass number are we using to handle year changes (ref)</param>
        /// <param name="query">Populated SPQuery object (ref)</param>
        /// <returns></returns>
        bool populateQuery(DateTime startDate, DateTime endDate, string SelectedResource, 
                ref bool multiplePass, ref int passNumber, ref SPQuery query)
        {
            bool returnValue = true;
            string queryText = "";


            query = new SPQuery();
            query.ExpandRecurrence = true;  // critical to finding other recurring appointments
            DateTime queryDate = startDate;
            multiplePass = false;
            


            string queryPeriod = "";
            if (startDate.Year == endDate.Year)
            {
                if (startDate.Month == endDate.Month)
                {
                    if (startDate.Day == endDate.Day)
                    {
                        queryPeriod = "<Week />";
                    }
                    else
                    {
                        queryDate = new DateTime(startDate.Year, startDate.Month, 1);
                        queryPeriod = "<Month />";
                    }

                }
                else
                {
                    multiplePass = true;
                    queryDate = new DateTime(startDate.Year, startDate.Month, 1);
                    queryDate = queryDate.AddMonths(passNumber);
                    if (queryDate > endDate)
                    {
                        returnValue = false;
                    }
                    else
                    {
                        queryPeriod = "<Month />";
                    }
                }
            }
            else
            {
                multiplePass = true;
                queryDate = new DateTime(startDate.Year, startDate.Month, 1);
                queryDate = queryDate.AddMonths(passNumber);
                if (queryDate > endDate)
                {
                    returnValue = false;
                }
                else
                {
                    queryPeriod = "<Month />";
                }
            }

            if (returnValue)
            {
                if (ContainsResource)
                {
                    // this query should be valid, but seems to ignore the resource criteria
                    queryText = "<Query>" + 
                                   "<Where>" +
                                      "<And>" +
                                      "<DateRangesOverlap>" +
                                           "<FieldRef Name=\"EventDate\" />" +
                                           "<FieldRef Name=\"EndDate\" />" +
                                           "<FieldRef Name=\"RecurrenceID\" />" +
                                           "<Value Type=\"DateTime\">" +
                                                queryPeriod +
                                            "</Value>" +
                                      "</DateRangesOverlap>" +
                                      "<Eq>" +
                                         "<FieldRef Name='" + RESOURCE_COLUMN + "' LookupId='TRUE' />" +
                                         "<Value Type='Lookup'>" + SelectedResource + "</Value>" +
                                      "</Eq>" +
                                      "</And>" +
                                   "</Where>" + 
                                "</Query>";


                }
                else
                {
                    queryText = "<Where>" +
                                   "<DateRangesOverlap>" +
                                       "<FieldRef Name=\"EventDate\" />" +
                                       "<FieldRef Name=\"EndDate\" />" +
                                       "<FieldRef Name=\"RecurrenceID\" />" +
                                       "<Value Type=\"DateTime\">" +
                                            queryPeriod +
                                        "</Value>" +
                                    "</DateRangesOverlap>" +
                                "</Where>";
                }
            }


            if (returnValue)
            {
                query.CalendarDate = queryDate;
                query.Query = queryText;
            }
            return (returnValue);
        }

        /// <summary>
        /// Performs the query on the list and adds all overlapping events to the overlapList DataTable
        /// </summary>
        /// <param name="theList">List being queried</param>
        /// <param name="startDate">Start Date</param>
        /// <param name="endDate">End Date</param>
        /// <param name="ItemID">ItemID to match for update, 0 for new</param>
        /// <param name="SelectedResource">Selected Resource</param>
        /// <param name="query">SPQuery object populated by populateQuery</param>
        /// <param name="overlapList">DataTable of overlapping items</param>
        /// <param name="errorMsg">Error messages to return to the caller</param>
        void AddQueryResultsToTable(SPList theList, DateTime startDate,
                DateTime endDate, int ItemID, string SelectedResource, 
                SPQuery query, ref DataTable overlapList, ref string errorMsg)
        {
            // query the list
            SPListItemCollection calendarItems = theList.GetItems(query);

            // go through the list returned by the query and check for overlaps
            foreach (SPListItem item in calendarItems)
            {
                if (item.ID != ItemID) // dont check the current item
                {
                    bool checkDates = true;
                    // query ignores the resource= clause, so have to filter manually
                    if (ContainsResource)
                    {
                        string itemResource = item[RESOURCE_COLUMN].ToString();
                        if (itemResource.Contains(";"))
                        {
                            if (ResourceDisplay == String.Empty)
                            {
                                ResourceDisplay = itemResource.Substring(itemResource.IndexOf(";") + 2);
                            }
                            itemResource = itemResource.Substring(0, itemResource.IndexOf(";"));
                            checkDates = (itemResource == SelectedResource);
                        }

                    }

                    if (checkDates)
                    {
                        DateTime eventStart = (DateTime)item[item.Fields.GetFieldByInternalName("EventDate").Id];
                        DateTime eventEnd = (DateTime)item[item.Fields.GetFieldByInternalName("EndDate").Id];

                        // does item overlap the start/end dates
                        if (!((startDate >= eventEnd) || (endDate <= eventStart)))
                        {
                            // this item overlaps the start/end dates

                            // already got enough overlaps
                            if (overlapList.Rows.Count > MAX_DUPS)
                            {
                                return;
                            }

                            // adding and populating a row to the overlapList DataTable
                            DataRow theRow = overlapList.NewRow();
                            theRow["Title"] = item.Title;
                            try
                            {

                                theRow["Start"] = eventStart;
                                theRow["End"] = eventEnd;

                                // extract user's display name
                                string modBy = item[item.Fields.GetFieldByInternalName("Editor").Id].ToString();
                                if (modBy.IndexOf("#") > 0)
                                {
                                    modBy = modBy.Substring(modBy.IndexOf("#") + 1);
                                }
                                theRow["Modified By"] = modBy;
                            }
                            catch (Exception ex) // general exception handler
                            {
                                errorMsg += ex.Message;
                            }
                            overlapList.Rows.Add(theRow);
                        } // if overlaps
                    } // check dates
                } // if not current item
            } // foreach
        } //end AddQueryResultsToTable

        /// <summary>
        /// Builds an ErrorMessage from the contents of the overlapList DataTable
        /// </summary>
        /// <param name="overlapList">Overlapping assignments table</param>
        /// <returns></returns>
        string formatErrorTable(DataTable overlapList)
        {
            System.Text.StringBuilder sBuilder = new System.Text.StringBuilder(); // used to build the error message
            sBuilder.Append("The following reservation");
            if (overlapList.Rows.Count == 1)
            {
                sBuilder.Append(" conflicts");
            }
            else
            {
                sBuilder.Append("s conflict");
            }
            sBuilder.Append("  with the desired reservation.<BR>");


            // go through the row collection and add the contents to the error message
            foreach (DataRow row in overlapList.Rows)
            {
                sBuilder.Append("Title: " + row["Title"].ToString() + "\t from " +
                    row["Start"].ToString() + " to " + row["End"].ToString());

                if(ContainsResource)
                {
                    sBuilder.Append(" for " + ResourceDisplay);
                }

                sBuilder.Append(" by " + row["Modified By"].ToString() + "<BR>");
            }

            // converts the message to a string to return it.
            return (sBuilder.ToString());
        }

        private void ValidateRecurrence(ref string XMLToValidate)
        {
            try
            {
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(XMLToValidate);
            }
            catch (Exception ex)
            {
                XMLToValidate = String.Empty;
            }
        }

        #endregion

        
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions