Delete trouble - recurring events (EF6)

6 posts, 0 answers
  1. Simon
    Simon avatar
    39 posts
    Member since:
    Mar 2016

    Posted 16 Jan 2018 Link to this post

    Using the SchedulerCustomEditor as a sample project I've created a project where the user can manage Events, both individual and recurring. I'm using EF6 to manage read/write to the database. My database has an Events table which has a many-to-many relationship with Categories and a many to many relationship with Locations (see attached). 

    Add and Edit events (and their related categories) seems to work fine, recurring events and recurrence exceptions are getting created, updated properly, delete works fine until  a recurring event has a recurrence exception. An exception is thrown by Entity Framework: "Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded." Stepping through my delete function the calls to delete the recurrence exception event and it's parent occur almost simultaneously, I've notice similar in the Delete function in the SchedulerCustomEditor sample project:

     

            public virtual void Delete(MeetingViewModel meeting, ModelStateDictionary modelState)
            {
                if (meeting.Attendees == null)
                {
                    meeting.Attendees = new int[0];
                }

                var entity = meeting.ToEntity();

                db.Meetings.Attach(entity);

                var attendees = meeting.Attendees.Select(attendee => new MeetingAttendee
                {
                    AttendeeID = attendee,
                    MeetingID = entity.MeetingID
                });

                foreach (var attendee in attendees)
                {
                    db.MeetingAttendees.Attach(attendee);
                }

                entity.MeetingAttendees.Clear();

                var recurrenceExceptions = db.Meetings.Where(m => m.RecurrenceID == entity.MeetingID);

                foreach (var recurrenceException in recurrenceExceptions)
                {
                    db.Meetings.Remove(recurrenceException);
                }

                db.Meetings.Remove(entity);
                db.SaveChanges();
            }

    Can't say I really understand what is going on in the above function with the Attach calls. In the above function you are manually removing the records MeetingAttendees join table somehow, but in my scenario I have no access to these join tables.

     

    My Delete function is:

     

            public virtual void Delete(EventScheduleViewModel evt, ModelStateDictionary modelState)
            {
                if (evt.Categories == null)
                {
                    evt.Categories = new int[0];
                }
                if (evt.Locations == null)
                {
                    evt.Locations = new int[0];
                }

                var entity = db.Events.Include("Categories").Include("Locations").FirstOrDefault(m => m.EventID == evt.EventID);

                foreach (var category in entity.Categories.ToList())
                {
                    entity.Categories.Remove(category);
                }

                foreach (var location in entity.Locations.ToList())
                {
                    entity.Locations.Remove(location);
                }

                var recurrenceExceptions = db.Events.Where(m => m.RecurrenceID == entity.EventID);

                foreach (var recurrenceException in recurrenceExceptions)
                {
                    db.Events.Remove(recurrenceException);
                }

                db.Events.Remove(entity);
                try
                {
                    db.SaveChanges();
                }

                catch (Exception e)
                {
                    throw;
                }
            }

     

    here's my Insert and Update functions which seem to be working ok:

     

            public virtual void Insert(EventScheduleViewModel evt, ModelStateDictionary modelState)
            {
                if (ValidateModel(evt, modelState))
                {
                    if (evt.Categories == null)
                    {
                        evt.Categories = new int[0];
                    }
                    if (evt.Locations == null)
                    {
                        evt.Locations = new int[0];
                    }

                    var entity = evt.ToEntity();

                    foreach (var categoryId in evt.Categories)
                    {
                        var category = db.Categories.FirstOrDefault(s => s.CategoryID == categoryId);
                        if (category != null)
                        {
                            entity.Categories.Add(category);
                        }
                    }

                    foreach (var locationId in evt.Locations)
                    {
                        var location = db.Locations.FirstOrDefault(s => s.LocationID == locationId);
                        if (location != null)
                        {
                            entity.Locations.Add(location);
                        }
                    }

                    try
                    {
                        db.Events.Add(entity);
                        db.SaveChanges();
                    }

                    catch (Exception)
                    {
                        throw;
                    }

                    evt.EventID = entity.EventID;
                }
            }

            public virtual void Update(EventScheduleViewModel evt, ModelStateDictionary modelState)
            {
                if (ValidateModel(evt, modelState))
                {

                    var entity = db.Events.Include("Categories").Include("Locations").FirstOrDefault(m => m.EventID == evt.EventID);

                    entity.Title = evt.Title;
                    entity.Start = evt.Start;
                    entity.End = evt.End;
                    entity.Description = evt.Description;
                    entity.IsAllDay = evt.IsAllDay;
                    entity.RecurrenceID = evt.RecurrenceID;
                    entity.RecurrenceRule = evt.RecurrenceRule;
                    entity.RecurrenceException = evt.RecurrenceException;
                    entity.StartTimezone = evt.StartTimezone;
                    entity.EndTimezone = evt.EndTimezone;


                    entity.Fee = evt.Fee;
                    entity.ContactName = evt.ContactName;
                    entity.ContactPhone = evt.ContactPhone;
                    entity.ContactEmail = evt.ContactEmail;
                    entity.Summary = evt.Summary;
                    entity.OffsiteLocation = evt.OffsiteLocation;
                    entity.SubmitterName = evt.SubmitterName;
                    entity.SubmitterPhone = evt.SubmitterPhone;
                    entity.SubmitterEmail = evt.SubmitterEmail;
                    entity.SubmitterComments = evt.SubmitterComments;
                    entity.ImagePath = evt.ImagePath;
                    entity.ImageAltText = evt.ImageAltText;

                    entity.LastModified = evt.LastModified;
                    entity.LastModifiedBy = evt.LastModifiedBy;

                    entity.IsOffCampus = evt.IsOffCampus;
                    entity.IsPublished = evt.IsPublished;
                    entity.IsDisplayedOnNSCC = evt.IsDisplayedOnNSCC;
                    entity.IsDisplayedOnConnectStudent = evt.IsDisplayedOnConnectStudent;
                    entity.IsDisplayedOnConnectEmployee = evt.IsDisplayedOnConnectEmployee;
                    entity.IsCollegeWide = evt.IsCollegeWide;


                    foreach (var category in entity.Categories.ToList())
                    {
                        entity.Categories.Remove(category);
                    }

                    foreach (var location in entity.Locations.ToList())
                    {
                        entity.Locations.Remove(location);
                    }

                    if (evt.Categories != null)
                    {
                        foreach (var categoryId in evt.Categories)
                        {
                            var category = db.Categories.FirstOrDefault(s => s.CategoryID == categoryId);
                            if (category != null)
                            {
                                entity.Categories.Add(category);
                            }
                        }
                    }

                    if (evt.Locations != null)
                    {
                        foreach (var locationId in evt.Locations)
                        {
                            var location = db.Locations.FirstOrDefault(s => s.LocationID == locationId);
                            if (location != null)
                            {
                                entity.Locations.Add(location);
                            }
                        }
                    }

                    try
                    {
                        db.SaveChanges();
                    }

                    catch (Exception)
                    {
                        throw;
                    }
                }
            }

     

    How can I write my Delete function to remove recurring events that have recurrence exceptions? 

     

    Thanks.

     

     

  2. Veselin Tsvetanov
    Admin
    Veselin Tsvetanov avatar
    1201 posts

    Posted 18 Jan 2018 Link to this post

    Hi Simon,

    Thank you for the detailed explanation of the case and for the snippets sent.

    The observed is caused by concurrency problems in the database. The issue is that when a Recurring event with Exception(s) is being deleted, multiple Destroy requests are sent to the remote. To avoid that, the Batch() mode of the Scheduler DataSource should be enabled:
    .DataSource(d => d
        .Batch(true)
        .Model(m => {
            m.Id(f => f.MeetingID);
            m.Field(f => f.Title).DefaultValue("No title");
            m.RecurrenceId(f => f.RecurrenceID);
        })
    ..........

    Moreover, the Create, Update and Destroy endpoints should also be updated to accept a collection of Meetings:
    public virtual JsonResult Meetings_Create([DataSourceRequest] DataSourceRequest request, IEnumerable<MeetingViewModel> models)
    {
    .....

    And the Destroy endpoint should skip the deletion of the exception, when the whole series is deleted:
    public virtual JsonResult Meetings_Destroy([DataSourceRequest] DataSourceRequest request, IEnumerable<MeetingViewModel> models)
    {
        if (ModelState.IsValid)
        {
            var list = models.ToList();
     
            for (int i = 0; i < list.Count; i++)
            {
                var meeting = list[i];
     
                if (meeting.RecurrenceID != null)
                {
                    for (int j = 0; j < list.Count; j++)
                    {
                        var potentialParent = list[j];
     
                        if (meeting.RecurrenceID == potentialParent.MeetingID) {
                            models = models.Where(m => m.MeetingID != potentialParent.MeetingID);
                        }
                    }
                }
            }
     
            foreach (var meeting in models)
            {
                meetingService.Delete(meeting, ModelState);
            }
        }
     
        return Json(models.ToDataSourceResult(request, ModelState));
    }

    Attached you will find a modified version of the sample project, implementing the above changes.

    As a small token of gratitude for bringing to our attention the above issue, I have updated your Telerik points.

    Regards,
    Veselin Tsvetanov
    Progress Telerik
    Try our brand new, jQuery-free Angular components built from ground-up which deliver the business app essential building blocks - a grid component, data visualization (charts) and form elements.
  3. Simon
    Simon avatar
    39 posts
    Member since:
    Mar 2016

    Posted 22 Jan 2018 Link to this post

    Ok thanks for the update, but it has created a new issue, or I'm missing something somewhere. I thought it might be related to the project upgrade to kendo.Mvc 2018.1.117.545 but I'm not positive. I implemented the changes as you described in the updated sample project you sent. Seemed straightforward enough but now in my create method in my controller the IEnumerable <EventScheduleViewModel> is null, so the add event fails. As the project upgrade creates a backup project (referencing kendo.Mvc 2017.3.913.545) I changed the code in the backup project to match and same problem, the the IEnumerable <EventScheduleViewModel> is null on the create method. So I don't think it's the upgrade.

    The sample project you sent works fine, I see 1 event in the IEnumerable on Create as expected. I have not upgraded this project.

    I have set     .Batch(true) on my DataSource in the index View. Here's my complete Index.cshtml:

    @using NSCC.Administration.Helpers

    @(Html.Kendo().Scheduler<NSCC.Administration.Models.EventScheduleViewModel>()
        .Name("scheduler")
        .Date(new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day))
        .StartTime(new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 7, 00, 00))
        .Height(600)
        .Views(views => {
            views.DayView();
            views.WeekView(weekView => weekView.Selected(true));
            views.MonthView();
            views.AgendaView();
        })
        .Editable(editable => {
            editable.TemplateName("CustomEditorTemplate");
            //editable.Window(w => w.Width(700));
            editable.Resize(true);
        })
        .Timezone("Etc/UTC")
        .Resources(resource => {
            resource.Add(m => m.Categories)
                .Title("Categories")
                .Multiple(true)
                .DataTextField("Name")
                .DataValueField("CategoryID")
                .DataSource(source =>
                {
                    source.Read(read => { read.Action("Read_Categories", "EventSchedule"); });
                });
            resource.Add(m => m.Locations)
                .Title("Locations")
                .Multiple(true)
                .DataTextField("Name")
                .DataValueField("LocationID")
                .DataSource(source =>
                {
                    source.Read(read => { read.Action("Read_Locations", "EventSchedule"); });
                });
        })
        .DataSource(d => d
            .Batch(true)
            .Model(m => {
                m.Id(f => f.EventID);
                m.Field(f => f.Title).DefaultValue("Event title");
             //   m.Field(f => f.ImagePath);
                //    m.Field(f => f.End);
                //    m.Field(f => f.IsAllDay);
                //    m.Field(f => f.Title);
                //    m.Field(f => f.RecurrenceRule);
                //    m.Field(f => f.Description);
                //    //m.Field(f => f.StudentID);
                //    //m.Field(f => f.ProcedureCode);
                //    //m.Field(f => f.Color);
                //    m.Field(f => f.Summary);
                m.RecurrenceId(f => f.RecurrenceID);
            })
            .Events(e => e.Error("error_handler"))
            .Read("EventSchedule_Read", "EventSchedule")
            .Create("EventSchedule_Create", "EventSchedule")
            .Destroy("EventSchedule_Destroy", "EventSchedule")
            .Update("EventSchedule_Update", "EventSchedule")
        )
        .Events(e =>
        {
            e.DataBound("scheduler_dataBound");
            e.DataBinding("scheduler_dataBinding");
            e.Edit("scheduler_edit");
            e.Save("scheduler_save");
        })
    )


    <script type="text/javascript">

        $(document).ready(function () {

        });


        function error_handler(e) {
            if (e.errors) {
                var message = "Errors:\n";
                $.each(e.errors, function (key, value) {
                    if ('errors' in value) {
                        $.each(value.errors, function () {
                            message += this + "\n";
                        });
                    }
                });
                alert(message);

                var scheduler = $("#scheduler").data("kendoScheduler");
                scheduler.one("dataBinding", function (e) {
                    //prevent saving if server error is thrown
                    e.preventDefault();
                })
            }
        }

        //manually update the Model with the ImagePath text field value
        function scheduler_save(e) {
            e.event.ImagePath = $("#ImagePath").val();
            e.event.dirty = true;
        }

        function scheduler_edit(e) {
            //Handle the add/edit event.
            //   console.log("Editing", e.event.title);

            // for an Add use either: if(e.event.id == 0) or:
            if (e.event.isNew()) {
                //  $("#custom-description").hide();
            }

            //hide the timezone selections
            e.container.find("[data-role=timezoneeditor]").hide();
            e.container.find("[data-role=timezoneeditor]").parent('div').hide();
            $('label[for="StartTimezone"]').hide();
            $('label[for="StartTimezone"]').parent('div').hide();
            e.container.find("label[for='EndTimezone']").hide();
            e.container.find("label[for='EndTimezone']").parent('div').hide();

            // remove Never form the End selection in the recurrence editor



            //remove the recurrence editor if the user selected "Edit Current Occurrence"
            if (!e.event.isNew() && e.event.recurrenceId) {
                e.container.find("[data-role=recurrenceeditor]").hide();
                $('label[for="recurrenceRule"]').hide();
            }

            $("#ImagePath").prop("readonly", true);

            $("#clearImage").bind("click", function (e) {
                e.preventDefault();
                $("#ImagePath").val("");
            });

            // we hide/show certain elements based on admin level
            var adminlevel = $("#adminlevel").data("adminlevel");
            if (adminlevel != "Events") {

                //hide the Display on Nscc.ca checkbox and
                $('label[for="IsDisplayedOnNSCC"]').hide();
                $('label[for="IsDisplayedOnNSCC"]').parent('div').hide();
                $('#IsDisplayedOnNSCC').hide();
                $('#IsDisplayedOnNSCC').parent('div').hide();

                //hide the College-wide check box and it's label and containing divs
                $('label[for="IsCollegeWide"]').hide();
                $('label[for="IsCollegeWide"]').parent('div').hide();
                $('#IsCollegeWide').hide();
                $('#IsCollegeWide').parent('div').hide();
            }

            $("#IsOffCampus").on("click", function () {
                var isChecked = $(this).is(":checked");

                if (isChecked) {
                    $("#OffsiteLocation").prop("disabled", false);
                }
                else {
                    $("#OffsiteLocation").val("");
                    $("#OffsiteLocation").prop("disabled", true);
                }
            });

            var dataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: '@(ViewBag.EventsImageService)',
                        dataType: "jsonp"
                    }
                },
                pageSize: 3
            });

            setTimeout(function () {
                $("#pager").kendoPager({
                    dataSource: dataSource
                });

                $("#listView").kendoListView({
                    dataSource: dataSource,
                    selectable: "single",
                    template: kendo.template($("#template").html()),
                    change: function (e) {

                        var data = dataSource.view(),
                            selected = $.map(this.select(), function (item) {
                                var sel = data[$(item).index()].FileName;
                              //  console.log(sel);
                                $("#ImagePath").val(sel);
                            });
                      //  console.log("ListView Change Event");
                    }
                });
            });

        }

        function scheduler_dataBound(e) {
            //Handle the dataBound event.
        }

        function scheduler_dataBinding(e) {
            //Handle the dataBinding event.
        }
    </script>

    Thanks for the help.

     

    Simon

     

     

     

     

     

  4. Veselin Tsvetanov
    Admin
    Veselin Tsvetanov avatar
    1201 posts

    Posted 24 Jan 2018 Link to this post

    Hello Simon,

    In order to be able to further assist you in troubleshooting the described issue, I will need to be able to reproduce it locally. Therefore, I would like to ask you to modify the project attached to my previous post, so it reproduces the problem observed and send it back to us.

    Regards,
    Veselin Tsvetanov
    Progress Telerik
    Try our brand new, jQuery-free Angular components built from ground-up which deliver the business app essential building blocks - a grid component, data visualization (charts) and form elements.
  5. Simon
    Simon avatar
    39 posts
    Member since:
    Mar 2016

    Posted 24 Jan 2018 Link to this post

    I found the problem, but it took a while. After changing the datasource to use .Batch(true) I was getting null in the Create, Edit, and Delete methods in the controller. My method signatures were:

    public virtual JsonResult EventSchedule_Create([DataSourceRequest] DataSourceRequest request, IEnumerable<EventScheduleViewModel> evts)

    public virtual JsonResult EventSchedule_Update([DataSourceRequest] DataSourceRequest request, IEnumerable<EventScheduleViewModel> evts)

    public virtual JsonResult EventSchedule_Destroy([DataSourceRequest] DataSourceRequest request, IEnumerable<EventScheduleViewModel> evts)

    I stripped out much of the code around the controller (this project is in a large Administration VS solution with many other projects and Authentication built in), finally in desperation I changed the parameter name from evts to models and it all works. You had this in your sample project you sent me but I overlooked it thinking that parameter names would not matter, apparently they REALLY matter!

    public virtual JsonResult EventSchedule_Create([DataSourceRequest] DataSourceRequest request, IEnumerable<EventScheduleViewModel> models)
    public virtual JsonResult EventSchedule_Update([DataSourceRequest] DataSourceRequest request, IEnumerable<EventScheduleViewModel> models)
    public virtual JsonResult EventSchedule_Destroy([DataSourceRequest] DataSourceRequest request, IEnumerable<EventScheduleViewModel> models)

    I could get away with evt as the parameter name when I expected a single Model instead of an IEnumerable, this worked:

    public virtual JsonResult EventSchedule_Create([DataSourceRequest] DataSourceRequest request, EventScheduleViewModel evt)

    But as soon as I add .Batch(true) to the DataSource the parameter must be named "models" it seems.

    Did I miss something in the docs? Some named parameter jquery is looking for somewhere? What gives?

     

    Simon

     

     

     

  6. Veselin Tsvetanov
    Admin
    Veselin Tsvetanov avatar
    1201 posts

    Posted 26 Jan 2018 Link to this post

    Hello Simon,

    I am really happy to hear, that you have identified the cause and have resolved the issue faced.

    When using the batch ​option of the DataSource, the changed data items are sent as models by default. This default behaviour could be changed via the parameterMap option. Here you will find some more info about the batch option.

    Regards,
    Veselin Tsvetanov
    Progress Telerik
    Try our brand new, jQuery-free Angular components built from ground-up which deliver the business app essential building blocks - a grid component, data visualization (charts) and form elements.
Back to Top