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
}
}