This tutorial shows how to create an ASP.NET timetable application that supports recurring events.
This is an advanced version of the basic ASP.NET timetable tutorial.
Sample Project
The sample project includes:
C# Source Code
VB.NET Source Code
Visual Studio 2010/2012 Solution
Features
Basic timetable features:
Based on the DayPilot event calendar ASP.NET control
Overview of events scheduled for the selected week
Changing the selected week using a date navigator
Time blocks with customizable start and end time (inline editing using header active areas)
Drag and drop event creating
Drag and drop event moving
Drag and drop event resizing
Custom event background color
Editable event description
Full calendar CSS support
Custom CSS theme
Storing data in an SQL Server database (sample database included)
C# source code of the sample project
VB source code of the sample project
Advanced timetable features (recurrence):
Support for recurring events
Recurring events are marked with a special icon
Customizable recurrence rules (daily, weekly, monthly, annually)
Exceptions from the rules (different time, description, color for a specified occurrence)
License
Licensed for testing and evaluation purposes. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for ASP.NET WebForms.
Requirements
.NET Framework 4.0 or higher
Visual Studio 2019 or higher (optional)
Microsoft SQL Server (Express)
Timetable Database Schema
We will add a special [AssignmentRecurrence] varchar field to the [Assignment] table. It will store the recurrence information (for master events and exceptions).
CREATE TABLE [dbo].[Assignment](
[AssignmentId] [bigint] IDENTITY(1,1) NOT NULL,
[AssignmentNote] [varchar](2000),
[AssignmentStart] [datetime] NOT NULL,
[AssignmentEnd] [datetime] NOT NULL,
[AssignmentColor] [varchar](50),
[AssignmentRecurrence] [varchar](250),
CONSTRAINT [PK_Assignment] PRIMARY KEY CLUSTERED
(
[AssignmentId] ASC
)
)
The [Block] table:
CREATE TABLE [dbo].[Block](
[BlockId] [int] NOT NULL,
[BlockStart] [datetime] NOT NULL,
[BlockEnd] [datetime] NOT NULL,
CONSTRAINT [PK_Block] PRIMARY KEY CLUSTERED
(
[BlockId] ASC
)
)
Displaying Recurring Events in the Timetable
DayPilot Calendar has built-in support for recurring events. It will automatically expand the individual occurrences from the rule and apply all exceptions (deleted or modified occurrences).
We will specify the database field that stores recurrence information using DataRecurrenceField property:
<DayPilot:DayPilotCalendar
ID="DayPilotCalendar1"
runat="server"
DataEndField="AssignmentEnd"
DataStartField="AssignmentStart"
DataTextField="AssignmentNote"
DataValueField="AssignmentId"
DataRecurrenceField="AssignmentRecurrence"
/>
In order to mark the recurring events in the timetable we will use BeforeEventRender event and add an icon to the upper-right corner of the recurring events:
C#
protected void DayPilotCalendar1_BeforeEventRender(object sender, DayPilot.Web.Ui.Events.Calendar.BeforeEventRenderEventArgs e)
{
if (e.Recurrent)
{
if (e.RecurrentException)
{
e.Areas.Add(
new Area()
.Right(5)
.Top(5)
.Width(14)
.Height(14)
.Visibility(AreaVisibility.Visible)
.CssClass("area_recurring_ex")
);
}
else
{
e.Areas.Add(
new Area()
.Right(5)
.Top(5)
.Width(14)
.Height(14)
.Visibility(AreaVisibility.Visible)
.CssClass("area_recurring")
);
}
}
}
VB
Protected Sub DayPilotCalendar1_BeforeEventRender(ByVal sender As Object, ByVal e As DayPilot.Web.Ui.Events.Calendar.BeforeEventRenderEventArgs)
Dim color As String = CStr(e.DataItem("AssignmentColor"))
If Not String.IsNullOrEmpty(color) Then
e.BackgroundColor = color
e.BorderColor = color
e.FontColor = "#ffffff"
End If
If e.Recurrent Then
If e.RecurrentException Then
e.Areas.Add((New Area()).Right(5).Top(5).Width(14).Height(14).Visibility(AreaVisibility.Visible).CssClass("area_recurring_ex"))
Else
e.Areas.Add((New Area()).Right(5).Top(5).Width(14).Height(14).Visibility(AreaVisibility.Visible).CssClass("area_recurring"))
End If
End If
End Sub
Timetable CSS Theme
The timetable_simple CSS theme was created using the online CSS theme designer. It uses the default colors and styles similar to the built-in CSS theme (calendar_default).
You can edit and download the CSS theme here:
It is manually customized to display smaller font in the time header (used for the block names):
.timetable_simple_rowheader_inner
{
font-size: 12pt;
text-align: right;
position: absolute;
top: 0px;
left: 0px;
bottom: 0px;
right: 0px;
border-right: 1px solid #aaaaaa;
border-bottom: 1px solid #aaaaaa;
color: #666666;
background: #eeeeee;
background: -webkit-gradient(linear, left top, right top, from(#f3f3f3), to(#e9e9e9));
background: -webkit-linear-gradient(left, #f3f3f3 0%, #e9e9e9);
background: -moz-linear-gradient(left, #f3f3f3 0%, #e9e9e9);
background: -ms-linear-gradient(left, #f3f3f3 0%, #e9e9e9);
background: -o-linear-gradient(left, #f3f3f3 0%, #e9e9e9);
background: linear-gradient(left, #f3f3f3 0%, #e9e9e9);
filter: progid:DXImageTransform.Microsoft.Gradient(startColorStr="#f3f3f3", endColorStr="#e9e9e9", GradientType=1);
}
Special CSS classes for active areas indicating event recurrence were added at the end of timetable_simple.css:
.area_recurring
{
background: no-repeat url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAIAAACQKrqGAAAAKnRFWHRDcmVhdGlvbiBUaW1lAHN0IDE5IElJIDIwMTQgMDk6NTU6MTAgKzAxMDCM4OYVAAAAB3RJTUUH3gITCDcVL0VpiQAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAARnQU1BAACxjwv8YQUAAAE4SURBVHjahVI/64JAGPZPi1QOErhEs4tfoy38IEE4uAji4OLnCAJx0UC/QCAu5pSWq+FWCtqYYW/Y7zhp+D3D3fO+73N37z13ZNd1BIb7/V6WJUmSs9mM4zi8RHR/yLJMlmW8pGna9XqFkmVZ5/P5K/V9Hyk2m816vUahYRgwHg6Hj/R4PPbZ3W73eDz6xU3TbLdbtCAMQ+L5fC6XSwhc1+2GcBwHSYMgINI0BSZJ0uv1wnVJkkB+sVjM53MgcPIoz3Ngq9WKoij8ToIgQDMoHI/Ho7ZtgTEMQwxB0/RkMsEzFM/zMMVxTPyLuq57cjqduh/cbjfEP2bZtg3S6XR6uVxwXRRFuDPfJ1BVtd9b13XP8/b7vaIofcY0zYEUAP7/toebTeLfpaoq6LgoCjAOHBVFkWVZVH0Dxfr9q0syMB0AAAAASUVORK5CYII=);
}
.area_recurring_ex
{
background: no-repeat url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAIAAACQKrqGAAAAKnRFWHRDcmVhdGlvbiBUaW1lAHN0IDE5IElJIDIwMTQgMDk6NTk6NTkgKzAxMDCHDk2yAAAAB3RJTUUH3gITCQAI3YbPUwAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAARnQU1BAACxjwv8YQUAAAGmSURBVHjaZVFNywFhFJ0ZryTzIpGU/AH8gfkBdpKyVEOWSn6AhSysbBRLZaFEYjsbCwuyYOUzO1I2aHxtGOY948mTeu9i5s6955459x5WVdXn86nT6Zh37Pf7w+HAsqzdbrfZbMx3XK/XUqk0HA6Xy2U6nf5uZTKZzWYDrnq9Pp/PNZpCoZDNZgOBAEGkUqlkMkkHcrkcnr1ej8UQXq1WC3+0Wq2JRILnefQul0un0xFFkQwMBgNOUZR8Pl+tVg0GgyzL/X6f9H7fQblfrxczm82QhUKh8/kM0VDSbDbRmE6nqHs8HrfbjWQ0Gv2s12tkwWAQHLFYDDhsAD3hcBgaKKvJZNIEIDMajeQ7Go2CCWgIhST+Ezgf53Q6ARqPx2Rar9dTdK1WezwelJg9nU4WiwXZZDLx+XykClPa7TbQLpcrHo9jHkXObDY3Gg1kgiDABQKFeZFIBLjdblcsFm+3m1bEyl6v936/d7vdcrmMM6OxWCwqlQrccjgcOPZqtQKGUT8BZcy/wHKSJMFOmKq5RRvH4xGKt9stx3HYzO/3Qx7q+A+O8wenyt+j7JPKzQAAAABJRU5ErkJggg==);
}
The inline data URL is used to store the image data (14x14 PNG):
.area_recurring
.area_recurring_ex
Adding Events to the Timetable
The users can add new events to the timetable by selecting the blocks using a mouse.
A new event dialog will open:
The dialog is extended with the "Repeat" section that allows to specify the recurrence rule:
<div class="label">Repeat:</div>
<!-- Recurrence section -->
<script type="text/javascript" src="Scripts/DayPilot/recurrence.js"></script>
<select id="repeat">
<option value="norepeat">Does not repeat</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="annually">Annually</option>
</select>
<div id="select_norepeat" style="display:none">
</div>
<div id="select_daily" style="display:none">
Repeat every <input id="daily_every" style="width: 20px;" value="1" /> day(s).
</div>
<div id="select_weekly" style="display:none">
Repeat every <input id="weekly_every" style="width: 20px;" value="1" /> week(s).
<div id="select_weekly_error" style="display:none; margin-top: 5px; padding: 2px; background-color: #FFF1A8;">Please select at least one day:</div>
<br />
<table>
<tr>
<td>On days: </td>
<td><label for="weekly_0"><input type="checkbox" id="weekly_0" />Sun</label></td>
<td><label for="weekly_1"><input type="checkbox" id="weekly_1" />Mon</label></td>
<td><label for="weekly_2"><input type="checkbox" id="weekly_2" />Tue</label></td>
<td><label for="weekly_3"><input type="checkbox" id="weekly_3" />Wed</label></td>
<td><label for="weekly_4"><input type="checkbox" id="weekly_4" />Thu</label></td>
<td><label for="weekly_5"><input type="checkbox" id="weekly_5" />Fri</label></td>
<td><label for="weekly_6"><input type="checkbox" id="weekly_6" />Sat</label></td>
</tr>
</table>
</div>
<div id="select_monthly" style="display:none">
Repeat every <input id="monthly_every" style="width: 20px;" value="1" /> month(s).
<div id="select_monthly_error" style="display:none; margin-top: 5px; padding: 2px; background-color: #FFF1A8;">Please select at least one day:</div>
<br />
<label for="monthly_1"><input type="checkbox" id="monthly_1" />1</label>
<label for="monthly_2"><input type="checkbox" id="monthly_2" />2</label>
<label for="monthly_3"><input type="checkbox" id="monthly_3" />3</label>
<label for="monthly_4"><input type="checkbox" id="monthly_4" />4</label>
<label for="monthly_5"><input type="checkbox" id="monthly_5" />5</label>
<label for="monthly_6"><input type="checkbox" id="monthly_6" />6</label>
<label for="monthly_7"><input type="checkbox" id="monthly_7" />7</label>
<label for="monthly_8"><input type="checkbox" id="monthly_8" />8</label>
<label for="monthly_9"><input type="checkbox" id="monthly_9" />9</label>
<label for="monthly_10"><input type="checkbox" id="monthly_10" />10</label>
<label for="monthly_11"><input type="checkbox" id="monthly_11" />11</label>
<label for="monthly_12"><input type="checkbox" id="monthly_12" />12</label>
<label for="monthly_13"><input type="checkbox" id="monthly_13" />13</label>
<label for="monthly_14"><input type="checkbox" id="monthly_14" />14</label>
<label for="monthly_15"><input type="checkbox" id="monthly_15" />15</label>
<label for="monthly_16"><input type="checkbox" id="monthly_16" />16</label>
<label for="monthly_17"><input type="checkbox" id="monthly_17" />17</label>
<label for="monthly_18"><input type="checkbox" id="monthly_18" />18</label>
<label for="monthly_19"><input type="checkbox" id="monthly_19" />19</label>
<label for="monthly_20"><input type="checkbox" id="monthly_20" />20</label>
<label for="monthly_21"><input type="checkbox" id="monthly_21" />21</label>
<label for="monthly_22"><input type="checkbox" id="monthly_22" />22</label>
<label for="monthly_23"><input type="checkbox" id="monthly_23" />23</label>
<label for="monthly_24"><input type="checkbox" id="monthly_24" />24</label>
<label for="monthly_25"><input type="checkbox" id="monthly_25" />25</label>
<label for="monthly_26"><input type="checkbox" id="monthly_26" />26</label>
<label for="monthly_27"><input type="checkbox" id="monthly_27" />27</label>
<label for="monthly_28"><input type="checkbox" id="monthly_28" />28</label>
<label for="monthly_29"><input type="checkbox" id="monthly_29" />29</label>
<label for="monthly_30"><input type="checkbox" id="monthly_30" />30</label>
<label for="monthly_31"><input type="checkbox" id="monthly_31" />31</label>
</div>
<div id="select_annually" style="display:none">
</div>
<div id="range" style="display:none">
<div style="border-bottom: 2px solid #d0d0d0; margin-top:10px; margin-bottom: 10px;"></div>
<label for="repeat_indefinitely"><input type="radio" name="repeat_range" id="repeat_indefinitely" />Repeat indefinitely</label><br />
<label for="repeat_until"><input type="radio" name="repeat_range" id="repeat_until" />Repeat until: </label><input id="repeat_until_value" style="width: 150px;" value="12/31/2099" /><br />
<label for="repeat_times"><input type="radio" name="repeat_range" id="repeat_times" />Repeat </label><input id="repeat_times_value" style="width: 20px;" /> time(s).<br />
</div>
<script type="text/javascript">
var r = new DayPilot.Recurrence();
r.saveButtonId = "ButtonOK";
r.jsonHiddenId = "Recurrence";
r.config = <%= RecurrenceJson %>;
r.onSelect = function(args) {
if (typeof modal !== 'undefined') {
modal.stretch();
}
};
r.Init();
</script>
<!-- Recurrence section ends here -->
It is a template that can be customized using your own styles and arrangement.
The recurrence.js helper (DayPilot.Recurrence class) saves the recurrence rule to a hidden field called "Recurrence" before submitting so it can be read in the ButtonOK.Click event handler.
C#
protected void ButtonOK_Click(object sender, EventArgs e)
{
DateTime day = Convert.ToDateTime(TextBoxDay.Text);
int start = Convert.ToInt32(DropDownListStart.SelectedValue);
int duration = Convert.ToInt32(DropDownListDuration.SelectedValue);
string note = TextBoxNote.Text;
string color = DropDownListColor.SelectedValue;
string recurrence = Recurrence.Value;
int totalBlocks = TimetableManager.TotalBlocks;
duration = start + duration > totalBlocks ? totalBlocks - start : duration; // make sure it's within the visible range
DateTime startDate = day.Add(TimetableManager.MapBlock(start));
DateTime endDate = startDate.Add(TimetableManager.MapBlock(duration));
new DataManager().CreateAssignment(startDate, endDate, note, color, recurrence);
// passed to the modal dialog close handler, see Scripts/DayPilot/event_handling.js
Hashtable ht = new Hashtable();
ht["refresh"] = "yes";
ht["message"] = "Event created.";
Modal.Close(this, ht);
}
VB
Protected Sub ButtonOK_Click(ByVal sender As Object, ByVal e As EventArgs)
Dim day As Date = Convert.ToDateTime(TextBoxDay.Text)
Dim start As Integer = Convert.ToInt32(DropDownListStart.SelectedValue)
Dim duration As Integer = Convert.ToInt32(DropDownListDuration.SelectedValue)
Dim note As String = TextBoxNote.Text
Dim color As String = DropDownListColor.SelectedValue
Dim recurrence_Renamed As String = Recurrence.Value
Dim totalBlocks As Integer = TimetableManager.TotalBlocks
duration = If(start + duration > totalBlocks, totalBlocks - start, duration) ' make sure it's within the visible range
Dim startDate As Date = day.Add(TimetableManager.MapBlock(start))
Dim endDate As Date = startDate.Add(TimetableManager.MapBlock(duration))
CType(New DataManager(), DataManager).CreateAssignment(startDate, endDate, note, color, recurrence_Renamed)
' passed to the modal dialog close handler, see Scripts/DayPilot/event_handling.js
Dim ht As New Hashtable()
ht("refresh") = "yes"
ht("message") = "Event created."
Modal.Close(Me, ht)
End Sub
The DataManger.CreateAssignment() method uses RecurrenceRule.FromJson() to parse the rule and RecurrenceRule.Encode() converts it to the string that will be stored in the database (AssignmentRecurrence field).
C#
public void CreateAssignment(DateTime start, DateTime end, string note, string color, string recurrenceJson)
{
using (DbConnection con = CreateConnection())
{
con.Open();
var cmd = CreateCommand("insert into [Assignment] ([AssignmentStart], [AssignmentEnd], [AssignmentNote], [AssignmentColor]) values (@start, @end, @note, @color)", con);
AddParameterWithValue(cmd, "start", start);
AddParameterWithValue(cmd, "end", end);
AddParameterWithValue(cmd, "note", note);
AddParameterWithValue(cmd, "color", color);
cmd.ExecuteNonQuery();
cmd = CreateCommand("select @@identity;", con);
int id = Convert.ToInt32(cmd.ExecuteScalar());
RecurrenceRule rule = RecurrenceRule.FromJson(id.ToString(), start, recurrenceJson);
string recurrenceString = rule.Encode();
if (!String.IsNullOrEmpty(recurrenceString))
{
cmd = CreateCommand("update [Assignment] set [AssignmentRecurrence] = @recurrence where [AssignmentId] = @id", con);
AddParameterWithValue(cmd, "recurrence", rule.Encode());
AddParameterWithValue(cmd, "id", id);
cmd.ExecuteNonQuery();
}
}
}
VB
Public Sub CreateAssignment(ByVal start As Date, ByVal [end] As Date, ByVal note As String, ByVal color As String, ByVal recurrenceJson As String)
Using con As DbConnection = CreateConnection()
con.Open()
Dim cmd = CreateCommand("insert into [Assignment] ([AssignmentStart], [AssignmentEnd], [AssignmentNote], [AssignmentColor]) values (@start, @end, @note, @color)", con)
AddParameterWithValue(cmd, "start", start)
AddParameterWithValue(cmd, "end", [end])
AddParameterWithValue(cmd, "note", note)
AddParameterWithValue(cmd, "color", color)
cmd.ExecuteNonQuery()
cmd = CreateCommand("select @@identity;", con)
Dim id As Integer = Convert.ToInt32(cmd.ExecuteScalar())
Dim rule As RecurrenceRule = RecurrenceRule.FromJson(id.ToString(), start, recurrenceJson)
Dim recurrenceString As String = rule.Encode()
If Not String.IsNullOrEmpty(recurrenceString) Then
cmd = CreateCommand("update [Assignment] set [AssignmentRecurrence] = @recurrence where [AssignmentId] = @id", con)
AddParameterWithValue(cmd, "recurrence", rule.Encode())
AddParameterWithValue(cmd, "id", id)
cmd.ExecuteNonQuery()
End If
End Using
End Sub
The RecurrenceRule requires the event id when creating the rule so we need to
save the event first
read its database-generated ID using "select @@identity", and
create the rule
store the encoded rule in AssignmentRecurrence
Note that these three SQL commands should be executed as a single transaction (it is not implemented in this sample).
Timetable Event Recurrence Rules
Recurrence rules supported:
daily (every X days)
weekly (every X weeks, on specified weekdays)
monthly (every X months, on specified days of month)
annually (very X years)
Daily Rule
Weekly Rule
Monthly Rule
Annually Rule
Editing the Series of an Occurrence
If an event is a part of a recurrence rule we need to ask for the editing mode when opening the event for editing:
<DayPilot:DayPilotCalendar
ID="DayPilotCalendar1"
runat="server"
...
EventClickHandling="JavaScript"
EventClickJavaScript="ask(e)"
...
/>
The ask() function checks the event type:
function ask(e) {
// it's a normal event
if (!e.recurrent()) {
edit(e);
return;
}
// it's a recurrent event but it's an exception from the series
if (e.id() !== null) {
edit(e);
return;
}
var modal = new DayPilot.Modal();
modal.closed = function () {
if (this.result != "cancel") {
edit(e, this.result);
}
};
modal.showUrl("RecurrentEditMode.html");
}
RecurrentEditMode.html page displays the options and returns "this" or "series" to the ask() function:
Do you want to edit this occurrence or the series?
<div><label for="this"><input type="radio" name="result" value="this" id="this" checked="checked" /> This occurrence only</label></div>
<div><label for="series"><input type="radio" name="result" value="series" id="series" /> Whole series</label></div>
<div style="margin-top:10px">
<button onclick="javascript:clicked(selected());">OK</button>
<button onclick="javascript:clicked('cancel');">Cancel</button>
</div>
</div>
<script type="text/javascript">
function selected() {
if (document.getElementById("this").checked) {
return "this";
}
return "series";
}
function clicked(result) {
if (parent && parent.DayPilot && parent.DayPilot.ModalStatic) {
parent.DayPilot.ModalStatic.close(result);
}
}
</script>
The edit() function passes details about the event (master id, occurrence start) to the Edit.aspx page:
function edit(e, mode) {
var url = "Edit.aspx?q=1"
if (e.recurrentMasterId()) {
url += "&master=" + e.recurrentMasterId();
}
if (e.value() !== null) {
url += "&id=" + e.id();
}
if (mode == "this") {
url += "&start=" + e.start();
}
createModal().showUrl(url);
}
Editing Recurring Events
The code for event editing is much more complex that usually because we need to handle four possible events types:
a master event (we are editing the rule)
an regular occurrence (we will create a new exception when saved)
an exceptional occurrence (this one is already stored as a special record in the database)
a regular event (no recurrence)
C#
protected void ButtonOK_Click(object sender, EventArgs e)
{
switch (Mode)
{
case EventMode.Master:
SaveMaster();
break;
case EventMode.NewException:
SaveNewException();
break;
case EventMode.Exception:
SaveException();
break;
case EventMode.Regular:
SaveRegular();
break;
default:
throw new ArgumentOutOfRangeException();
}
Hashtable ht = new Hashtable();
ht["refresh"] = "yes";
ht["message"] = "Event updated.";
Modal.Close(this, ht);
}
VB
Protected Sub ButtonOK_Click(ByVal sender As Object, ByVal e As EventArgs)
Select Case Mode
Case EventMode.Master
SaveMaster()
Case EventMode.NewException
SaveNewException()
Case EventMode.Exception
SaveException()
Case EventMode.Regular
SaveRegular()
Case Else
Throw New ArgumentOutOfRangeException()
End Select
Dim ht As New Hashtable()
ht("refresh") = "yes"
ht("message") = "Event updated."
Modal.Close(Me, ht)
End Sub
Something similar applies to the "Delete" button:
C#
protected void ButtonDelete_Click(object sender, EventArgs e)
{
switch (Mode)
{
case EventMode.Master:
new DataManager().DeleteAssignment(MasterId);
break;
case EventMode.Exception:
new DataManager().DeleteAssignment(Id);
break;
case EventMode.NewException:
new DataManager().CreateAssignmentExceptionDeleted(MasterId, Start);
break;
case EventMode.Regular:
new DataManager().DeleteAssignment(Id);
break;
}
Hashtable ht = new Hashtable();
ht["refresh"] = "yes";
ht["message"] = "Event deleted.";
Modal.Close(this, ht);
}
VB
Protected Sub ButtonDelete_Click(ByVal sender As Object, ByVal e As EventArgs)
Select Case Mode
Case EventMode.Master
CType(New DataManager(), DataManager).DeleteAssignment(MasterId)
Case EventMode.Exception
CType(New DataManager(), DataManager).DeleteAssignment(Id)
Case EventMode.NewException
CType(New DataManager(), DataManager).CreateAssignmentExceptionDeleted(MasterId, Start)
Case EventMode.Regular
CType(New DataManager(), DataManager).DeleteAssignment(Id)
End Select
Dim ht As New Hashtable()
ht("refresh") = "yes"
ht("message") = "Event deleted."
Modal.Close(Me, ht)
End Sub