By Peter Bell

Creating a OO Calendar: Part 1

There are a number of great posts on building a Calendar. There are also some promising looking projects on RIA Forge, but they are a little too self contained and don't drop easily into an existing MVC architected framework.

I needed to add calendar functionality to my in-house framework. Here is how I went about repurposing the resources out there to fit into my system . . .

What do you Need?
I decided to start with a simple use case. I need the ability to display schedulable objects on a month to view calendar (I'll probably add week, business week and day to-view over time). If you think about it, a calendar is really just a fancy list view (as it is responsible for displaying a list of 0..n object instances based on a filter).

Model Methods
My first thought was to create some kind of BaseEvent object which my various schedulable objects could extend. About two seconds later I came to my senses and decided to favor composition over inheritance and to describe an ISchedulable interface that schedulable objects could implement. If you don't know why I did this, imagine purchasable, schedulable objects like training courses - do they extend BaseProduct or BaseEvent? I think IPurchasable and ISchedulable will serve me much better (I'm not actually going to implement this using interfaces in CF8, but the concept of interfaces allows me to specify the metadata required to automagically generate/interpret the appropriate methods using mixins without having to actually write any code for the implementations on a per business object basis.)

What methods do I need ISchedulable to implement? getbyMonth() is really all I need to get started (with the obvious getByDay(), getbyWeek() and getbyBusinessWeek() when I decide to expand the system). As per most of my core methods, I'm going to support a PropertyNameList so you can describe what properties of the business object you'd like returned and also a general filter method in case you want to build features like filtering by state or project type or favorite sport or professional qualifications required or whatever. This gives us getByMonth(Month:int:required, Year:int:required, PropertyNameList:string:required, Filter:string:optional) as the summary interface description.

What would this code look like? Borrowing heavily, the core code is just:

<cffunction name="getByMonth" returntype="any" hint="I return an IBO based on the filter provided in the order requested containing a single page of data based on the page number and number of records per page." output="false">
   <cfargument name="Month" type="string" required="false" default="" hint="The month to view.">   
   <cfargument name="Year" type="string" required="false" default="" hint="The year to view.">   
   <cfargument name="Filter" type="string" required="true" hint="The filter to use.">   
   <cfargument name="Order" type="string" required="false" default="#DefaultOrderBy#" hint="The order by using 1..n property names with optional desc after each.">   
   <cfargument name="PropertyNameList" type="string" required="false" default="#DefaultPropertyNameList#" hint="An optional comma delimited list of the property names to load the IBO with.">   
   <cfargument name="IncludeHidden" type="boolean" required="false" default="false" hint="Whether to include hidden records.">   
   <cfargument name="IncludeUnapproved" type="boolean" required="false" default="false" hint="Whether to include upapproved records.">   
   <cfargument name="IncludeDeleted" type="boolean" required="false" default="false" hint="Whether to include deleted records in the list.">   
   <cfscript>
      var Object = "";
      var MonthStart = "";
      var MonthEnd = "";
      arguments.Order = "StartDate";
      If (Len(arguments.Filter))
      arguments.Filter = arguments.Filter & " AND ";
      If (Len(arguments.Month) LT 1)
      {arguments.Month = DatePart("m", now());};
      If (Len(arguments.Year) LT 1)
      {arguments.Year = DatePart("yyyy", now());};
      MonthStart = DateFormat(CreateDate(Year, Month, 1), "MM/DD/YYYY");
      MonthEnd = DateFormat(CreateDate(Year, Month, DaysInMonth(MonthStart)), "MM/DD/YYYY");
      // Get all events that start before end of month and end after start of month       arguments.Filter = arguments.Filter & "(StartDate <= '#MonthEnd#' AND EndDate >= '#MonthStart#')";
      Object = getByFilter(argumentCollection=arguments);
   </cfscript>
   <cfreturn Object>
</cffunction>

Processing the List
Earlier I said that an events calendar really just displays a list of events. In theory that it true but when you try to actually write the display code to display a month, you realize that running queries of queries (or even worse, true database queries) for every day doesn't seem like an optimal way to get all of the events for a given day.

There are lots of ways of turning the list of events with start and end dates into something that is easier to display. I have some ideas about what to do, but no definitive best practice. Hmm, so I'm not sure the best way to process the list of events into something that is easy to display, and the way I do it might change. Sounds like a little bit of encapsulation might be in order. How about we create a special "calendarEventList" object?

What might that object look like? Well, what kind of things do I need it to do? Initial thoughts for an interface for my simple month to view use case include:

  • loadEvents(EventRecordset: recordset) - Load a collection of events into the object using a recordset from a cfquery. Should be able to be run multiple times to load multiple recordsets (fixed events, recurring events, etc.).
  • getDaysEvents(Date: date) - Returns a list of events for the day (what format? Array? Recordset? object?)
  • dayHasEvents(Date: date) - Returns a boolean as to whether a given day has any events (may affect display).
  • isToday(Date: date) - I think it'd be nice to wrap most of the logic in the bean to keep the layout focused on display, so I'll add a simple isToday() boolean method to the bean that takes a date and returns whether or not it is today. I can always remove this if it seems inappropriate down the line.

There are a few nice things about having an object here. Firstly we can run multiple passes to load it up, so we can query for simple events but then also write separate queries to load recurring events and the like, keeping each query nice and simple. We can also encapsulate any timezone and date formatting craziness within the bean (for now this is US only, server timezone only, but at least we have a place to hide the algorithms when we need to internationalize this). Finally, we can try different internal representations of the events using anything from queries to structs of arrays to XML objects with XPath to handle the storage of the events and we can vary the storage within the bean completely independently of changing the queries to load the bean or the methods to retrieve information from the bean. Sounds like a promising start.

To try this out, we need to select some kind of way to store the events. I really liked what Ben was doing using Fix() to create an integer version of every date which could be treated as keys in the structure. In his case he was only interested in whether a given date had any events. We want to be able to store a list of events under each day. At first I was thinking an array, but that is a pain to sort as what we really want to get is all of the events in a given day ordered by their start times (if they have any, and with some kind of rule if some events don't have a start time - and we're probably going to have to be a little clever about events that started before today in terms of displaying them first - but those are details we can handle in phase 2). So, what is the best way to handle this? Two solutions come to mind. The cleanest (to me) seems to be just putting an object into each key something like calendarDayEventList.cfc. It would have methods to addEvent() and to getEvents(). The other approach would be to just create a QueryNew() for each struct key and add records to the query, including an OrderBy query of queries as part of the code that returns the recordset to the display screen to process it. I think the dayEventList object is the "best" approach, so I'm going to start out with that. If performance becomes a issue (it's only up to 30 transients per request, so it shouldn't be TOO painful), I know how to optimize the performance by changing out the code to use recordsets or something else instead . . .

I'm gonna go do some project work and then later this week I'll put together some code to try this out. In the meantime, any thoughts/ideas/input appreciated!

Comments
This one's pretty good and quite mature: http://kalendar.riaforge.org/
# Posted By Sebastiaan | 10/16/07 6:32 AM
Thanks - have been meaning to check it out ever since I saw Dan Wilson had joined the project. This blog entry is from before I knew about Kalendar, so I'd definitely recommend anyone to check the project out.
# Posted By Peter Bell | 10/16/07 6:36 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.005.