Offering a list of upcoming and even past events is a very important feature a successful business' online presence can offer its patrons. Everyday life for people keeps getting faster and faster paced and keeping track of everything one needs to do is hard enough, knowing when they can have fun can easily get lost. Providing patrons easy access to a schedule of events at the Beer House is a great way method to boost response in the pub. Giving customers the means to quickly add events to their calendar as reminders is another great way to remind them of the fun things they can do after they have worked all day or all week long.
The Beer House tries to maintain a regular schedule of popular events and activities to increase traffic in the pub on a nightly basis. Fortunately with sporting events and holidays the schedule has many nights covered. But the Beer House also likes to fill in the night where there is not something already scheduled that will attract folks in such as Karaoke, Ladies night and live entertainment. Promoting these events in the bar is one thing, but it is common for customers to forget these activities once they leave the bar, especially if they have enjoyed a bit too much of the Beer!
Because most of the potential Beer House customers are active online keeping an up to date schedule available on the web site is very important to keeping them informed. It gives them a way to quickly check to see what is happening and refer their friends as well. Offering a natural way to add a reminder to their digital calendars is another great way to let them be more active in scheduling their time to include a trip to the Beer House.
Here's the list of features needed in the Events module:
Display a list of upcoming events.
Display events in a calendar format.
Allow visitors to see full event details.
Allow visitors to easily add an event reminder to their calendar.
Promote upcoming events in the common layout of the site.
For some events allow customers to RSVP.
The Events Module consists of two tables, one for Events and one for Event RSVPs. Instead of calling the event table Event, I called it EventInfo
because using the name Event tends to cause issues because Event is a keyword in the .NET languages.
The tbh_EventInfo
table contains a title, description, event time and duration, location, an AllowRegistration
fields. The tbh_EventRSVP
table contains some basic fields to collect information about the user attending, or not attending the event. The tables are related by a one-to-many relationship on the EventId
field (see Figure 10-1).
As in the previous chapters a dedicated Entity Data Model is generated for the Event module, called CalendarofEvents
. It contains an entity for EventInfo
and EventRSVP
, shown in Figure 10-2. And as usual the EntitySet
and relationships need to be renamed to something more friendly. The module namespace is Bll.EventCalendar
, so after the model is generated this namespace needs to be added to the accompanying designer file.
Like the other module business layers, each of the tables has corresponding entities and repositories. Each one inherits from the BaseEventRepository
(see Figure 10-3) that manages the disposing members and wraps the DataContext in a common property.
Like all the entity repositories in the site, the EventRepository
contains members to do basic retrieval, inserting, and updating of EventInfo
entities. There are a few custom retrieval methods to retrieve a list of events based on a specified date. They're described in the following table.
Method | Description |
---|---|
Retrieves a full list of events | |
Retrieves a list of active events | |
Retrieves an event by its EventId | |
Returns the number of events in the database | |
Adds a new event to the database | |
UpdateEventInfo | Updates an existing event |
Deletes an event by setting the Active flag to false | |
UnDeletes an event by setting the Active flag to true | |
Returns a list events for the day passed to the method | |
Returns a list events for the current day | |
Retrieves a list of events after the day passed to the method |
The EventRSVPRepository
contains the basic repository members to perform CRUD operations against the entity data model. Here's a look at them:
Method | Description |
---|---|
Retrieves a full list of poll optionss | |
Retrieves a list of active poll options by the specified PollId | |
Retrieves a list of poll options by the specified PollId | |
Retrieves the PollOption by the specificied PollOptionId | |
Returns a count of poll options | |
Adds a new poll option to the database | |
Updates an existing poll option | |
Deletes a poll option by setting the Active flag to false | |
Undeletes a poll by setting the Active flag to true |
Following are the pages and controls that constitute the user interface layer of this module:
~/Admin/ManageEvents.aspx: An administrative list of events with icons to edit or delete the event.
~/Admin/AddEditEvent.aspx: Allows adding or editing of an event.
~/Admin/ManageEventRSVPs.aspx: An administrative list of event RSVPs with icons to edit or delete the RSVP. The list can be edited by event.
~/Admin/AddEditEventRSVP.aspx: Allows a site admin to either add or edit an event RSVP.
~/BrowseEvents.aspx: List upcoming events and displays a calendar that highlights days with events scheduled.
~/ShowEvent.aspx: Displays the details of the event, with a link to RSVP is the event allows registrations.
The EventiCal Httphandler: Returns an iCal card to the user that can be imported into Outlook or personal information manager.
Let's look at the user control now, and we'll cover the ASP.NET pages as we go through the rest of the chapter.
The vCalendar Httphandler
control sends an iCal card to the user. iCalendar is based on the vCalendar standard defined to exchange personal calendar information. Often used in group situations to coordinate meetings, free/busy services allow users to publish their schedules to others. The Beer House can use this to enable patrons to easily track what is happening in their favorite watering hole, which also meets the overall goals of distributing information about the Beer House. Fortunately distributing this type of information is very easy with a custom HttpHandler
because it is just a formatted text document. For more information, see Request for Comment (RFC) 2445 (www.ietf.org/rfc/rfc2445.txt
), which defines the format, and RFCs 2446 and 2447 (www.ietf.org/rfc/rfc2446.txt
and www.ietf.org/rfc/rfc2447.txt
), which define interoperability of the content between systems and free/busy services.
Several years ago I found a couple of useful projects on CodeProject.com
(www.codeproject.com/KB/vb/vcalendar.aspx
) to easily manage vCalendar
and vCard
documents. They work by adding values to the object graph that represents the data format, vCalendar
in this case. Once the data has been added to the object calling the ToString
method returns the properly formatted data as a string. This string can be returned however desired, for a web site this means setting the MIME type and adding the resulting string to the output stream. The MIME type for an iCalendar
file is "text/calendar", which tells the browser which application should consume the resource being loaded. For most people this will be Outlook, but with the proliferation of mobile devices this could be almost anything, and because this is an open standard format it is nothing we need to be concerned with.
The handler's ProcessRequest
method checks to make sure there is an EventId
value passed in the QueryString
. If not it calls the SendNoEventMessage
to the browser, a nice message to the user letting them know what is wrong without throwing a hard exception that may not help the end user.
If Not IsNothing(Request.QueryString("EventId")) Then BuildVCal() Else SendNoEventMessage() End If Private Sub SendNoEventMessage() 'Instead of throwing an error, let the user know what they did wrong in a graceful manner.CurrentContext CurrentContext.Response.ContentType = "text/HTML" CurrentContext.Response.Write(""P"Sorry, Please supply a valid EventId."/P"") CurrentContext.Response.Flush() CurrentContext.Response.End() Exit Sub End Sub
If an EventId
value is present the BuildVCal
method is called. It again does a quick test to make sure the EventId
value is positive, if not the SendNoEventMessage
is called. Next the event is retrieved from the database and the values are passed to a vCalendar
object. The way the vCalendar
works is it can contain multiple vEvent
and vAlarm
objects, both defined within the vCalendar
class. The handler is concerned with just a single event, so only one vEvent
object is created. The Description, Location
, and URL
properties are set first. Then the event date and times are set.
Setting the event date and time takes a little extra effort because the event start and end times are defined in separate database fields from the start and end dates. A new composite DateTime
value must be created for each. The logic has to be aware of empty end dates and times. The time fields are stored as a string, so I used a regular expression to parse the Hour, Minute and AM/PM values. I chose to keep this pretty simple, assuming the data should be formatted in the format according to the MaskEditExtender
I will talk about in the Solution section.
Be aware the EventEndDate
value is a Nullable
type and therefore you need to convert this to a real DateTime
class using the GetValueOrDefault
method. If the EventEndDate
is null, then the EventDate
is assumed to be the EventEndDate
. This will be the case for most events because they should be more like appointments that last a few minutes or a few hours.
Null values are a common problem for programmers in any language and platform. Null dates in particular seem to be a common issue developers experience when they start dealing with null values. The .NET framework provides three things that can help the situation: nullable types in C# and VB.NET, a coalesce operator in C# (int x = y ?? 5), and the inline If statement in VB.NET (Dim x as integer = If(y, 5)).
In .NET value types can be declared as a nullable type, which ultimately gets compiled to the Nullable(of T)
structure. This structure has four members: Value, HasValue, GetValueOrDefault
and GetValueOrDefault(defaultValue as T)
.
When the Entity Data Model Wizard encounters a field that allows null values, it creates the corresponding property as a nullable value:
Public Property EventEndDate() As Global.System.Nullable(Of Date) Get Return Me._EventEndDate End Get Set(ByVal value As Global.System.Nullable(Of Date)) Me.OnEventEndDateChanging(value) Me.ReportPropertyChanging("EventEndDate") Me._EventEndDate = Global.System.Data.Objects.DataClasses .StructuralObject.SetValidValue(value) Me.ReportPropertyChanged("EventEndDate") Me.OnEventEndDateChanged() End Set End Property
In addition to using the GetValueOrDefault
member I have also found the use of the Coalesce functionality to be very valuable. In C# this is represented by ??; here's an example:
int? x = null; int y = x ?? 5;
VB.NET uses a hybrid of the if
statement to manage coalescing:
Dim x as Integer? = Nothing Dim y as Integer = if(x, 5)
This is why the following line is used to set the actual event end date:
Dim dtEnd As DateTime = If(lEventInfo.EventEndDate, lEventInfo.EventDate) Private Sub BuildVCal() Using lEventrpt As New EventRepository Dim lEventId As Integer = CInt(Request.QueryString("EventId")) If lEventId <= 0 Then SendNoEventMessage() End If Dim lEventInfo As EventInfo = lEventrpt.GetEventInfoById(lEventId) If Not IsNothing(lEventInfo) Then Dim lvCal As New vCalendar Dim lEvent As New vCalendar.vEvent lEvent.Description = lEventInfo.EventTitle
lEvent.Location = lEventInfo.EventLocation lEvent.URL = String.Format("{0}?eventid={1}", _ Path.Combine(Helpers.WebRoot, "ShowEvent.aspx"), _ lEventId) lEvent.Summary = lEventInfo.EventDesc Dim rgTime As New Regex("^(d{2}):(d{2}):(d{2}) (PM|AM)$") Dim mHour As Integer = 0 Dim mMinute As Integer = 0 For Each m As Match In rgTime.Matches(lEventInfo.EventTime) If m.Groups.Count > 3 Then If m.Groups(4).Value = "PM" Then mHour = 12 + m.Groups(1).Value Else mHour = m.Groups(1).Value End If mMinute = m.Groups(2).Value End If Next lEvent.DTStart = New Date(lEventInfo.EventDate.Year, lEventInfo.EventDate.Month, lEventInfo.EventDate.Day, _ mHour, mMinute, 0) mHour = 0 mMinute = 0 For Each m As Match In rgTime.Matches(lEventInfo.EndTime) If m.Groups.Count > 3 Then If m.Groups(4).Value = "PM" Then mHour = 12 + m.Groups(1).Value Else mHour = m.Groups(1).Value End If mMinute = m.Groups(2).Value End If Next 'EventEndDate is a Nullable type, so we need to convert it to a hard date and time. Dim dtEnd As DateTime = If(lEventInfo.EventEndDate, lEventInfo.EventDate) If mHour > 0 Then
lEvent.DTEnd = New Date(dtEnd.Year, dtEnd.Month, dtEnd.Day, mHour, mMinute, 0) End If lvCal.Events.Add(lEvent) CurrentContext.Response.ContentType = "text/calendar" CurrentContext.Response.ContentEncoding = Text.Encoding.UTF8 CurrentContext.Response.Write(lvCal.ToString) CurrentContext.Response.Flush() CurrentContext.Response.End() Else SendNoEventMessage() End If End Using End Sub
Just like the other HttpHandlers
I have introduced in this version of the book, the custom handler needs to be registered in the web.config file and associated with a URL. For the iCalendar file type the standard extension is .ics
, so we will stick with that standard and associate any request for an .ics
file with the custom handler:
<add verb="*" path="*.ics" validate="false" type="TheBeerHouse.vCalHandler, TBHBLL, Version=3.5.0.1, Culture=neutral, PublicKeyToken=null"/>
With these pieces in place, iCalendar files can be streamed dynamically from the Beer House's web site with the click of a hyperlink and added to the patron's personal schedule Figure 10-4.
Again, we will discuss the details of implementing the Calendar of Events modules, but not repeat common patterns and features that have already been explored in previous chapters.
For the most part the business layer repositories follow the same patterns for the CRUD operations and will not be reviewed. Custom methods for each one will be added, but for the most part the module is implemented with very basic functionality.
In addition to the basic CRUD methods that follow the same patterns used in previous repositories there are three custom functions in the EventRepository
to return a list of events based on specified dates.
The GetDaysEvents
function runs a LINQ to Object query over the results of the GetActiveEvents
function to return just a list of events for the specified day. The GetTodaysEvents
function returns a list of events for the current date by passing the current date to the GetDaysEvents
function. The results of the GetActiveEvents
are cached (assuming that is enabled), so these methods should be very fast because they are querying against an in-memory list.
Public Function GetTodaysEvents() As List(Of EventInfo) Return GetDaysEvents(Today) End Function Public Function GetDaysEvents(ByVal vDate As DateTime) As List(Of EventInfo) Return (From lEventInfo In GetActiveEvents() Where lEventInfo.EventDate = vDate).ToList End Function
The GetUpcomingEvents
function retrieves a list of events occurring on the current day and after. Because it is a LINQ statement, the EventDate
property can be compared against the Today value using the "= operator. Notice I am still using the Active=True filter. I tend to do this because you will very rarely work with inactive records in the database; remember the default GetEvents
or get a list method in each repository does not apply this filter, so retrieving and working with inactive data can always be done.
Public Function GetUpcomingEvents() As List(Of EventInfo) Dim key As String = CacheKey & "_Upcoming_n" & Today If EnableCaching AndAlso Not IsNothing(Cache(key)) Then Return CType(Cache(key), List(Of EventInfo)) End If
Dim lEvents As List(Of EventInfo) Eventctx.EventInfos.MergeOption = Objects.MergeOption.NoTracking lEvents = (From lEventInfo In Eventctx.EventInfos _ Where lEventInfo.EventDate "= Today _ And lEventInfo.Active = True).ToList() If EnableCaching Then CacheData(key, lEvents) End If Return lEvents End Function
Outside the standard repository methods, the EventRSVPRepository
has a the GetEventRSVPByEventId
method, which returns a list of EventRSVP
entities for the specified event.
Public Function GetEventRSVPByEventId(ByVal EventId As Integer) As List(Of EventRSVP) Dim key As String = CacheKey & "_List_By_EventId_" & EventId If EnableCaching AndAlso Not IsNothing(Cache(key)) Then Return CType(Cache(key), List(Of EventRSVP)) End If Dim lEventRSVPs As List(Of EventRSVP) Eventctx.EventRSVPs.MergeOption = Objects.MergeOption.NoTracking lEventRSVPs = (From lEventRSVP In Eventctx.EventRSVPs.Include("EventInfo") _ Where lEventRSVP.Active = True And lEventRSVP.EventInfo.EventId = EventId).ToList() If EnableCaching Then CacheData(key, lEventRSVPs) End If Return lEventRSVPs End Function
There is not much beyond what has already been covered as far as entity patterns to extend the base entity generated by the Entity Data Model Wizard. So I will omit filling these pages with the source code and advise you to review the previous chapters where I go into detail about the patterns employed by the partial class extensions of the generated entities.
The IsValid
property of the EventRSVP
entity checks to make sure the person's First and Last name have been supplied, if not it returns false.
Public ReadOnly Property IsValid() As Boolean Implements IBaseEntity.IsValid Get If String.IsNullOrEmpty(Me.FirstName) = False And _ String.IsNullOrEmpty(Me.LastName) = False Then Return True End If Return False End Get End Property
The FullName
property concatenates the First and Last Name properties into a string for the full name for convenience. The EventTitle
property returns the event's title the person RSVPed, if the associated EventInfo
object was not retrieved, it returns "NA".
Public ReadOnly Property FullName() As String Get Return String.Format("{0} {1}", Me.FirstName, Me.LastName) End Get End Property Public ReadOnly Property EventTitle() As String Get If Not IsNothing(Me.EventInfo) Then Return EventInfo.EventTitle End If Return "NA" End Get End Property
Now it's time to build the user interface: the administration pages, the Upcoming Events user control, and the public pages. The module's administration pages consist of the usual list and Add/Edit pages for events and the corresponding RSVPs.
This page, located under the ~/Admin folder, allows the administrator to view a list of events, add a new event, edit and event, and delete an event. The page is composed of a ListView
surrounded by an UpdatePanel
to list the events. Each event list the event title and the date in the first column. The ListView
also contains the familiar Pencil icon that is a hyper link to edit the event and the delete icon is the familiar trash can icon. The RSVP column contains a hot linked image that opens the ManageEventRSVPs.aspx
page, filtered for the specified event. Figure 10-5 is a screenshot of the page.
The AddEditEvent.aspx
page provides the visual representation to manage the information about an event. The page uses some of the ASP.NET AJAX control toolkit controls and extenders to add to the user experiences of the page. Primarily it uses the CalendarExtender
and the MaskedEditExtender
. The use of these controls to help users enter valid dates was covered in Chapter 4. But for a quick review, the CalendarExtender
is used to display a Calendar control when the user clicks on the calendar icon to the right of the corresponding TextBox
. The MaskEditExtender
uses a mask, which looks somewhat like a regular expression, to format a date. For a date the Mask
property is set to "99/99/9999" and the MaskType
is set to Date.
The Allow Registration checkbox (near the bottom of Figure 10-6) is important to note simply because checking this control will enable users to RSVP for the event. If this is left unchecked, then registration is suppressed for visitors. Because some events need to have a projected attendance ahead of time for planning purposes, while others to do not, keep this in mind when defining an event.
The BrowseEvents.aspx
page uses a Calendar
control and a ListView
to display information about the event calendar. The Calendar is used to display highlighted days when events are scheduled. The ListView
displays a list of upcoming events, but when a date is selected on the Calendar
control a list of events for that date is displayed. The list displays the event title, date, start time, and location along with the title and a More Information link to the ShowEvent.aspx
page for the event. The actual business area of the page is wrapped in an UpdatePanel
to make filtering for specific days pretty seamless for the user's experience.
The Calendar
control is styled so the current day and days with events stand out from other days, which helps users know when events are scheduled:
<asp:Calendar ID="objCalendar" runat="server" BorderColor="Black" BorderStyle="Solid" BorderWidth="1px"> <TodayDayStyle CssClass="TodayDayStyle"></TodayDayStyle> <DayStyle CssClass="DayStyle"></DayStyle> <DayHeaderStyle CssClass="DayHeaderStyle"></DayHeaderStyle>
<TitleStyle CssClass="TitleStyle"></TitleStyle> <OtherMonthDayStyle CssClass="OtherMonthDayStyle"></OtherMonthDayStyle> </asp:Calendar>
Figure 10-7 shows the page in action.
The real work in the BrowseEvents.aspx
page is in the event handlers for the Calendar control. As the Calendar is being rendered the DayRender
event is executed, letting you format and add content to the cell for the day on the calendar. For the Beer House calendar, it checks to see if there are any events on the day and, if so, sets the background color to a dark red and the number to white. It also sets the IsSelectable
property to true. This is important because for a user to filter just for that specific day the cell has to have some sort of post back mechanism in place. The IsSelectable
property determines if this is the case. If there are no events on the day being rendered, then this is set to false, and the user cannot use this day to filter for events. Here's the code:
Protected Sub objCalendar_DayRender(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DayRenderEventArgs) Handles objCalendar.DayRender Using lEventrpt As New EventRepository Dim lEventList As List(Of EventInfo) = lEventrpt.GetDaysEvents(e.Day.Date.ToShortDateString()) If lEventList.Count > 0 Then
Dim EventStyle As New Style() With EventStyle .BackColor = System.Drawing.Color.DarkRed .Font.Bold = True .ForeColor = Drawing.Color.White End With e.Day.IsSelectable = True e.Cell.ApplyStyle(EventStyle) Else e.Day.IsSelectable = False End If End Using End Sub
When a user selects a day the SelectionChanged
event is fired, which calls the BindDaysEvents
method, passing the Calendar's SelectedDate
value. This in turn binds the list of events for the selected date to the ListView
.
Protected Sub objCalendar_SelectionChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles objCalendar.SelectionChanged BindDaysEvents(objCalendar.SelectedDate) End Sub
If the user pages the Calendar
control to a new month, the BindData
method is called, which resets the ListView
to the list of upcoming events:
Protected Sub objCalendar_VisibleMonthChanged(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.MonthChangedEventArgs) Handles objCalendar.VisibleMonthChanged BindData() End Sub
This is an often overlooked event to create a handler for because as the user pages through the months, it is a good idea to keep the ListView up to date. You could also create a method to bind just the events for the displayed month.
The BindDaysEvents
method binds the events for the specified date to the ListView
, and also shows or hides the DataPager
control depending on how many events are bound to the ListView
:
Private Sub BindDaysEvents(ByVal vDate As DateTime) Using lEventrpt As New EventRepository
Dim lEventList As List(Of EventInfo) = lEventrpt.GetDaysEvents(objCalendar.SelectedDate) If lEventList.Count > 0 Then lvEvents.DataSource = lEventList lvEvents.DataBind() Dim pagerBottom As DataPager = lvEvents.FindControl("pagerBottom") If Not IsNothing(pagerBottom) Then If lEventList.Count "= pagerBottom.PageSize Then pagerBottom.Visible = False Else pagerBottom.Visible = True End If End If Else lvEvents.Items.Clear() End If End Using End Sub
If an event has the AllowRegisration
flag set to true the user can RSVP for the event. A link to do so is available on the event's ShowEvent.aspx page and loads the MakeEventRSVP.aspx
page. This page operates almost exactly like an entity Add/Edit page in the site's Admin section. The main difference is the use of the MultiView
. Initially the registration form is displayed, but once the RSVP has been successfully submitted a confirmation view is displayed to the user.
<asp:MultiView runat="server" ID="mvRegistration"> <asp:View runat="server" ID="vRegister"> 'Registration Form </asp:View> <asp:View runat="server" ID="vConfirmation"> 'Confirmation Form </asp:View> </asp:MultiView>
Figure 10-8 shows the registration and confirmation views.
This chapter has presented a working solution for a calendar of events. The complete calendar of events module is made up of an administration console for managing the events and their corresponding RSVPs through a web browser. The module takes advantage of the Calendar
control to provide a rich display of upcoming events for the Beer House to share with its patrons. This module can easily be employed in many real-world sites as it is now, but of course you can expand and enhance it as desired. Here are a few suggestions:
Add the capability for users to forward event, iCalendar, reminders to friends.
Add an Event Type filter and identifier to an event definition. For example the Beer House might have live entertainment, sporting events, etc and filtering them by type might be helpful to patrons.
Correlate events with photo gallery albums.