Thanks for your reply, Georgi!
I have a solution that went a slightly different direction, but one I really like, and I will post the relevant code here, in case someone else wants to do this. I think this is a pretty critical feature, as most prime time calendar applications have some sort of "delete this and future events" in a series. Ok, here goes...
1. First things first. Add a Remove event handler in the Razor view to the scheduler...
.Events(e =>
{
e.Remove(
"onRemove"
);
})
2. Implement onRemove in the javascript file that accompanies the View...
function
onRemove(e) {
// On a remove, open the window to select delete options...
if
(e.event.recurrenceRule) {
recurringEvent = e.event;
var
window = $(
"#seriesDeleteOption"
).data(
"kendoWindow"
);
window.center();
window.refresh();
window.open();
e.preventDefault();
}
}
First, I only need to do this on recurring rule events. If so, I capture the event in a global variable, as I will need it later for the result of the dialog I create in order to ask which delete option the user wants. Speaking of which, open that dialog, centering and refreshing it. The preventDefault is there to make sure nothing happens with the event yet on the server.
3. I need to know which event the user clicked from which I want to start the delete in the series, so capture that with another event handler on the scheduler, some javascript, and a global javascript variable for the date of the actual event clicked. Here are the two global variables in the JS file.
var
selectedDate =
null
;
var
recurringEvent =
null
;
Here are the two events now on the scheduler...
.Events(e =>
{
e.Remove(
"onRemove"
);
e.Change(
"onChange"
);
})
function
onChange(e) {
// Store the selected event's date, in case we want to delete a series from this date forward...
selectedDate = e.start;
}
Keep in mind, your scheduler needs to have selection enabled. Add this to your Html.Kendo().Scheduler<>()
4. Define the Kendo dialog box somewhere in your View file. This dialog will show your sweet new delete options and be launched by the above javascript!
@(Html.Kendo().Window()
.Name(
"seriesDeleteOption"
)
.Title(
"Delete Series"
)
.LoadContentFrom(
"DeleteOptionView"
,
"Instructor"
)
.Modal(
true
)
.Draggable()
.Visible(
false
)
.Width(400)
.Resizable()
.Position(position => position.Left(100).Top(100)))
5. Add the controller action that supplies the HTML for the view. This code isn't exciting, but it doesn't have to be.
public
ActionResult DeleteOptionView()
{
return
PartialView(
"_DeleteSeriesOptions"
);
}
6. The actual partial view.
<
div
>
<
div
class
=
"deleteSeriesOption"
>
<
input
type
=
"radio"
name
=
"deleteSeriesOption"
value
=
"0"
checked
=
"checked"
/><
label
for
=
"0"
>Delete this event and future events in the series</
label
>
</
div
>
<
div
class
=
"deleteSeriesOption"
>
<
input
type
=
"radio"
name
=
"deleteSeriesOption"
value
=
"1"
/><
label
for
=
"0"
>Delete all events in the series</
label
>
</
div
>
<
div
class
=
"deleteSeriesSubmit"
>
<
a
id
=
"submitDeleteChoice"
onclick
=
"closeDeleteOptions()"
>Continue</
a
>
<
a
id
=
"cancelDeleteChoice"
onclick
=
"cancelDeleteOptions()"
>Cancel</
a
>
</
div
>
</
div
>
7. Here is the cancel and delete javascript handlers for the two links at the bottom of the custom dialog. Right now, I have two options, delete the whole series, or just the event clicked and all subsequent events.
function
closeDeleteOptions(e) {
var
dialog = $(
"#seriesDeleteOption"
).data(
"kendoWindow"
);
var
theDate = selectedDate;
var
theEvent = recurringEvent;
// Now get the event and remove it, adding the data so the server sees it...
dialog.close();
if
(theDate !=
null
&& theEvent !=
null
) {
var
scheduler = $(
"#scheduler"
).data(
"kendoScheduler"
);
// If we want this one and forward deleted, pass extra goodies to the controller...
if
($(
"input[name=deleteSeriesOption]:checked"
).val() == 0) {
theEvent.DeleteForwardOnly =
true
;
theEvent.DeleteForwardDate = theDate.toDateString();
}
// We are ready to send the event back to the server for deletion...
// scheduler.removeEvent() will display the "delete series" dialog...
// to get around that, I alter the datasource with the item directly and sync...
scheduler.dataSource.remove(theEvent);
scheduler.dataSource.sync();
setTimeout(
function
() { scheduler.dataSource.read(); }, 100);
}
}
function
cancelDeleteOptions(e) {
var
window = $(
"#seriesDeleteOption"
).data(
"kendoWindow"
);
window.close();
}
This is pretty important code. If everything important is not null, we are going to add the clicked event date (and a boolean I probably don't need) to the event payload that will go back to the server for the Destroy handler. Calling removeEvent on the scheduler directly, at this point, will launch the "delete this occurrence or series" Kendo dialog, and we are past that point already. So, I opted to remove the event entirely from the datasource and sync it with the server. This has the desired effect of invoking the Destroy event on the server. However, if the user selected "just this event and on..." I want the scheduler to display those events again, and hence the scheduler.dataSource.read() to refresh the view.
Here is the field code to add the extra two fields to the datasource in the View. You will have other fields on your schedule object, but for the sake of completeness...
.DataSource(d => d
.Model(m =>
{
m.Id(f => f.EventId);
m.Field(f => f.Title);
m.Field(f => f.Description);
m.Field(f => f.Start);
m.Field(f => f.End);
m.Field(f => f.RecurrenceID);
m.Field(f => f.RecurrenceRule);
m.Field(f => f.RecurrenceException);
m.Field(f => f.IsAllDay);
m.Field(f => f.DeleteForwardOnly);
m.Field(f => f.DeleteForwardDate);
})
.Read(
"ReadEvents"
,
"Instructor"
)
.Create(
"CreateEvent"
,
"Instructor"
)
.Destroy(
"DestroyEvent"
,
"Instructor"
)
.Update(
"UpdateEvent"
,
"Instructor"
)
.Events(e => e.Error(
"error_handler"
))
)
8. Ok, now the hard part, and the fun part. The server needs to take the data and alter the event's recurrence properties if the user selected "delete this and future..." Here is my DestroyEvent code, minus stuff specific to my business.
public
virtual
JsonResult DestroyEvent([DataSourceRequest] DataSourceRequest request, ScheduleEvent scheduledEvent)
{
if
(ModelState.IsValid)
{
try
{
if
(scheduledEvent.DeleteForwardOnly && !String.IsNullOrEmpty(scheduledEvent.DeleteForwardDate))
{
// Just update the event to change it's end date to the day before...
DateTime dateToStartDeleting = DateTime.MinValue;
if
(DateTime.TryParse(scheduledEvent.DeleteForwardDate,
out
dateToStartDeleting))
{
// Adjust the end date (with time) until it's the day before the series is to be nuked...
var untilDate = scheduledEvent.End;
untilDate = untilDate.AddDays((dateToStartDeleting - untilDate).TotalDays);
// Adjust the recurrence rule to go a day before the day the series was deleted til...
scheduledEvent.RecurrenceRule = AdjustRecurrenceRule(scheduledEvent.RecurrenceRule, untilDate);
InstructionalEvent.Update(scheduledEvent.ToInstructionalEvent());
}
}
else
{
InstructionalEvent.Delete(scheduledEvent.EventId);
}
}
catch
(Exception ex)
{
Error.Create(ex);
throw
;
}
}
return
Json(
new
[] { scheduledEvent }.ToDataSourceResult(request, ModelState));
}
Basically, I just want to adjust the recurrence rule to contain an "UNTIL=" item set to the day the user chose to end the series. That is the crux of my logic. All the rest of the solution has been to get my code to the point where on the server, I can adjust the recurrence rule of the event as such. The scheduledEvent.End property contains the ending time for the event on the date on which the event starts. So that part ended up being just adding days based on the date the user selected. I figured I had to remove any existing "COUNT=" or "UNTIL=" that already existed on the event, and replace them with the new one from this helper function.
protected
string
AdjustRecurrenceRule(
string
originalRecurrence, DateTime newEndDate)
{
string
newRule = originalRecurrence;
var newList = originalRecurrence.Split(
';'
).ToList();
// Get rid of any existing UNTIL rules...
if
(newList.Any(r => r.Contains(
"UNTIL="
)))
{
var index = newList.IndexOf(newList.Single(r => r.Contains(
"UNTIL="
)));
newList.RemoveAt(index);
}
// Get rid of any existing COUNT rules...
if
(newList.Any(r => r.Contains(
"COUNT="
)))
{
var index = newList.IndexOf(newList.Single(r => r.Contains(
"COUNT="
)));
newList.RemoveAt(index);
}
newList.Add(
"UNTIL="
+ newEndDate.ToUniversalTime().ToString(Constants.KendoDateFormat));
newRule = String.Join(
";"
, newList);
return
newRule;
}
And my sweet Kendo date formatting constant...
public
const
string
KendoDateFormat =
"yyyyMMdd'T'HHmmss'Z'"
;
That's about it. I like the solution because (1) it seems to work and (2) the code is fairly straightforward and could be extended if other options were required.
I hope this helps if someone else is looking for the same functionality from the Kendo scheduler, which has been an enormous time saver and extremely easy to extend.
Adam