Chapter 10. Calendar of Events

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.

Problem

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.

Design

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.

Designing the Database Tables

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).

Figure 10-1

Figure 10.1. Figure 10-1

Creating the Entity Data Model

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.

Figure 10-2

Figure 10.2. Figure 10-2

Designing the Business Layer

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.

Figure 10-3

Figure 10.3. Figure 10-3

The EventRepository

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

GetEvents

Retrieves a full list of events

GetActiveEvents

Retrieves a list of active events

GetEventInfoById

Retrieves an event by its EventId

GetEventInfoCount

Returns the number of events in the database

AddEventInfo

Adds a new event to the database

UpdateEventInfo

Updates an existing event

DeleteEventInfo

Deletes an event by setting the Active flag to false

UnDeleteEventInfo

UnDeletes an event by setting the Active flag to true

DaysEvents

Returns a list events for the day passed to the method

GetTodaysEvents

Returns a list events for the current day

GetUpcomingEvents

Retrieves a list of events after the day passed to the method

The EventRSVPRepository

The EventRSVPRepository contains the basic repository members to perform CRUD operations against the entity data model. Here's a look at them:

Method

Description

GetEventRSVPs

Retrieves a full list of poll optionss

GetActiveActiveEventRSVPs

Retrieves a list of active poll options by the specified PollId

GetEventRSVPById

Retrieves a list of poll options by the specified PollId

GetEventRSVPByEventId

Retrieves the PollOption by the specificied PollOptionId

GetEventRSVPCount

Returns a count of poll options

AddEventRSVP

Adds a new poll option to the database

UpdateEventRSVP

Updates an existing poll option

DeleteEventRSVP

Deletes a poll option by setting the Active flag to false

UnDeleteEventRSVP

Undeletes a poll by setting the Active flag to true

Designing the User Interface Services

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.

  • ~/MakeEventRSVP.aspx: Allows visitors to RSVP for an event.

  • 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

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.

Dealing with Null Values

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.

Figure 10-4

Figure 10.4. Figure 10-4

Solution

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.

Implementing the Repositories

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.

Implementing the EventRepository

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

Implementing the EventRSVPRepository

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

Extending the Entity Model Entities

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

Implementing the User Interface

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.

The ManageEvents.aspx page

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.

Figure 10-5

Figure 10.5. Figure 10-5

The AddEditEvent.aspx 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.

Figure 10-6

Figure 10.6. Figure 10-6

The BrowseEvents.aspx Page

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.

Figure 10-7

Figure 10.7. Figure 10-7

The BrowseEvents.aspx.vb Code-Behind File

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

MakeEventRSVP.aspx Page

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.

Figure 10-8

Figure 10.8. Figure 10-8

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset