Wednesday, June 17, 2015

Binding ScheduleView to Database part 7: Recurrence

ScheduleView provides the functionality to configure repeating appointments. The user has the ability to apply recurring scheduling patterns such as daily, weekly, monthly or set a range of recurrence from date to date. If you have a daily task, for example: call mom, you can create an appointment and set his RecurrenceRule.

It is recommended to read Telerik's documentation, they explains very clearly, but they explanation only how to work with in-memory data, while in the real-world we want working with database.

Relative to previous articles, this article is a little complicated, because it covers a lot of objects, and requires an understanding of the internal behavior of the machine. I worked hard to decipher the behavior required, including code-review of the Telerik's source code [of course: with Telerik JustDecompile].
But in the end, I got a very good result, readable and elegant code, and here it is.

The source code, is available on my GitHub reporitory, and i recommend to see it. Here, I'm focusing only the essentials, so as not to burden the reader.

1. Overview Recurrense


Recurrence has two main functionality:

  1. Pattern. this is the rule himself. For example, you may have to call mom every day, week or month, or every specific week days.
  2. Exceptions. If you don't want to call mom on a specific day, or you want to change the time of the call.
Let's start with a small diagram, that shows the interfaces.




As you can see, we have a chine of models:

  • IAppointment has a RecurrenceRule property.
  • RecurrenceRule has a collection of ExceptionOccourrence.
  • ExceptionOccourrence has only two properties:
    1. ExceptionDate - this is the the point of time that you want exclude from the rule.
    2. Appointment - this is the replacement appointment that replace the original occurrence. It can be Null if you want omit the original occurrence and no replace it with another appointment]
The Appointment which is the first link in the chain, is referred to hereinafter as just Appointment or MasterAppointment, while the last link is referred to as ExceptionAppointment.



2. Models

MasterAppointment and ExceptionAppointment both are Appointment, but they are not the same: MasterAppointment has an optional RecurrenceRule property, while Exception has a parent ExceptionOccurence and cannot have a RecurrenceRule.

On the other hand, MasterAppointment and ExceptionAppointment has a lot of common properties. Both have Start, End, Subject, Body, Importance, Category, Resources etc, and we doesn't want duplicate the code.

The solution is INHERITANCE.
Let's see the model diagram:


3. Database schema





Remarks:

  • Appointment table has one-to-one-or-zero relationship with RecurreceRules table.
  • RecurreceRules has one-to-many relationship with ExeptionOccourrences table.
  • ExeptionOccourrences has one-to-one-or-zero relationship with ExeptionAppointments table.
  • We cannot store Appointments and ExceptionAppointments in same table, to avoid circularity.

OK, enough theory, let's go into the code.

3. AppointmentModelBase.cs


This is an abstract class, contains all the common properties and logic of Appointment, such as Subject, body, Start, End, Category etc. In fact, it almost similar to an Appointment class, we used in the previous posts.


4. AppointmentModel.cs


This class represent the normal appointment [MasterAppointments]. It is derived from AppointmentModelBase with a few changes.

public class AppointmentModel : AppointmentModelBase, IAppointment, IObjectGenerator<IRecurrenceRule>
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public virtual RecurrenceRuleModel RecurrenceRule { get; set; }

    IRecurrenceRule IAppointment.RecurrenceRule
    {
        get { return this.RecurrenceRule; }
        set { this.RecurrenceRule = value as RecurrenceRuleModel; }
    }

    #region IObjectGenerator<IRecurrenceRule>

    IRecurrenceRule IObjectGenerator<IRecurrenceRule>.CreateNew(IRecurrenceRule item)
    {
        IRecurrenceRule rule = (this as IObjectGenerator<IRecurrenceRule>).CreateNew();
        rule.CopyFrom(item);
        return rule;
    }

    IRecurrenceRule IObjectGenerator<IRecurrenceRule>.CreateNew()
    {
        if (this.RecurrenceRule == null)
            this.RecurrenceRule = new RecurrenceRuleModel();

        return this.RecurrenceRule; ;
    }

    #endregion // IObjectGenerator<IRecurrenceRule>

remarks:

When the user click on "Edit recurrence" button on AppointmentDialog, a new IRecureenceRule is required. then, if the appointment implements IObjectGenerator interface, the new IRecurrenceRule will be created using the CreateNew() method. else, a new instance of Telerik.Windows.Controls.ScheduleView.RecurrenceRule will be generated.

Because we want use our RecurrenceRuleModel object, we implemented the IObjectGenerator interface.


5. RecurrenceRuleModel.cs

Crot and properties:

[Table("RecurrenceRules")]
public class RecurrenceRuleModel : IRecurrenceRule
{
    public RecurrenceRuleModel()
    {
        this.Exceptions = new HashSet<ExceptionOccurrenceModel>();
    }

    [Key]
    // this property is the Key and also the ForeignKey to Appointments table
    public int AppointmentId { get; set; }
    public string PatternString { get; set; }

    public virtual AppointmentModel MasterAppointment { get; set; }
    public virtual ICollection<ExceptionOccurrenceModel> Exceptions { get; set; }



IRecurrenceRule implementing:

#region IRecurrenceRule

    [NotMapped]
    public RecurrencePattern Pattern
    {
        get
        {
            RecurrencePattern pattern;
            RecurrencePatternHelper.TryParseRecurrencePattern(this.PatternString, out pattern);
            return pattern;
        }
        set
        {
            this.PatternString = RecurrencePatternHelper.RecurrencePatternToString(value);
        }
    }

    /// <summary>
    /// This method called when the user create new ExceptionAppointment.
    /// the method receive the master appointment, and copy his properties [Subject, Body etc] to new ExceptionAppointment.
    /// </summary>
    /// <param name="master">The master appointment</param>
    /// <returns>New <see cref="ExceptionAppointmentModel"/></returns>
    public IAppointment CreateExceptionAppointment(IAppointment master)
    {
        ExceptionAppointmentModel newApp = new ExceptionAppointmentModel();
        newApp.CopyFrom(master);
        return newApp;
    }

    ICollection<IExceptionOccurrence> IRecurrenceRule.Exceptions
    {
        get { return new ObjectModel.CollectionWrapper<IExceptionOccurrence, ExceptionOccurrenceModel>(this.Exceptions); }
    }

    #endregion // IRecurrenceRule


Remarks:

  • RecurrencePatternHelper is part of Telerik.Windows.Controls.ScheduleView.ICalendar namespace. 
  • CollectionWrapper is my own object, that can wrap a ICollection<DerivedType> to use it as ICollection<BaseType>. In our case, we has a property named "Excpetions" that his signature is ICollection<ExceptionOccurrenceModel> and we need wrap it as ICollection<IExceptionOccurrence>.


IObjectGenerator implementing:

These methods are used when a new Exception is requested.

#region IObjectGenerator<IExceptionOccurrence>

public IExceptionOccurrence CreateNew(IExceptionOccurrence item)
{
    IExceptionOccurrence newExcepOcc = this.CreateNew();
    newExcepOcc.CopyFrom(item);
    return newExcepOcc;
}

public IExceptionOccurrence CreateNew()
{
    ExceptionOccurrenceModel newExc = new ExceptionOccurrenceModel();
    return newExc;
}

#endregion // IObjectGenerator<IExceptionOccurrence>


6. ExceptionOccurrenceModel.cs


[Table("ExceptionOccurrences")]
public class ExceptionOccurrenceModel : IExceptionOccurrence
{
    public int Id { get; set; }
    public DateTime ExceptionDate { get; set; }

    [ForeignKey("RecurrenceRule")]
    public int RecurrenceRuleId { get; set; }

    public virtual RecurrenceRuleModel RecurrenceRule { get; set; }
    public virtual ExceptionAppointmentModel Appointment { get; set; }

    IAppointment IExceptionOccurrence.Appointment
    {
        get { return this.Appointment; }
        set { this.Appointment = value as ExceptionAppointmentModel; }
    }

7. ExceptionAppointmentModel.cs


[Table("ExceptionAppointments")]
public class ExceptionAppointmentModel : AppointmentModelBase
{
    [Key]
    // this property is used also as foreign key
    public int ExceptionOccurrenceId { get; set; }
    public virtual ExceptionOccurrenceModel ExceptionOccurrence { get; set; }

8. ViewModel

All along we talked about the Models. What with the ViewModel layer? In the ViewModel layer we need only a little change. When we are loading the appointments by DateSpan, we need to get also the appointments that has RecurrenceRule, and they can occur in this DateSpan. see the code below.
private void LoadAppointments(IDateSpan dateSpan)
{
    this.dateSpan = dateSpan;

    this.appointments.Clear();
    List<AppointmentModel> appointments = this.GetAppointmentsByRange(dateSpan);
    this.appointments.AddRange(appointments);
}

private List<AppointmentModel> GetAppointmentsByRange(IDateSpan dateSpan)
{

    List<AppointmentModel> result = new List<AppointmentModel>();

    /* keep in mind that we need query the database and the local.
        * for example: if we open some Appointment and change the start from 01/01/2015 to 15/03/2015,
        * my changes are not in the database, and it stores temporary in local.
        */

    // load the MasterAppointments
    context.Appointments.Where(app =>
        (app.Start >= dateSpan.Start && app.Start <= dateSpan.End)
        ||
        (app.End >= dateSpan.Start && app.End <= dateSpan.End))
        .Load();


    // load the old appointments that may Occurrence in this dateSpan;
    context.Appointments.Where(app =>
            app.Start <= dateSpan.Start && app.RecurrenceRule != null)
            .Load();


    // add the appointments that may Occurrence in this dateSpan
    foreach (AppointmentModel item in context.Appointments.Local)
    {
        if ((item.Start >= dateSpan.Start && item.Start <= dateSpan.End)
            ||
            (item.End >= dateSpan.Start && item.End <= dateSpan.End))
        {
            result.Add(item);
        }
        else if (RecurrenceHelper.IsAnyOccurrenceInRange(item.RecurrenceRule.PatternString, dateSpan.Start, dateSpan.End) && !result.Contains(item))
        {
            result.Add(item);
        }
    }

    // query and load the exceptions
    IQueryable<AppointmentModel> exceptioAppointments = from app in context.ExceptionAppointments
                                                        where (app.Start >= dateSpan.Start && app.Start <= dateSpan.End)
                                                        ||
                                                        (app.End >= dateSpan.Start && app.End <= dateSpan.End)
                                                        select app.ExceptionOccurrence.RecurrenceRule.MasterAppointment;

    foreach (AppointmentModel item in exceptioAppointments)
    {
        if (!result.Contains(item))
        {
            result.Add(item);
        }
    }

    return result;
}

No comments:

Post a Comment