Skip to main content
A newer version of this page is available. .
All docs
V20.2

Load Data on Demand

  • 10 minutes to read

The SchedulerControl allows you to batch load items on demand. When you use this feature, the Scheduler loads only the data for the visible interval. This improves the initial load time and memory consumption when the Scheduler is bound to a large data source.

Run Demo: On-Demand Data Loading

To load items on demand, handle the following events:

Item type

Event

Appointments

DataSource.FetchAppointments

Time Regions

DataSource.FetchTimeRegions

Time Interval

The event’s Interval property returns the time range for which to load the scheduler items. This interval is calculated from the start of the earliest item in the SchedulerControl.VisibleIntervals collection to the end of the latest item in the collection, and extended up to the DataSource.FetchRange value.

If the SchedulerControl.VisibleIntervals duration is longer than DataSource.FetchRange, the DataSource.FetchRange value is ignored. The default DataSource.FetchRange value is 1 month.

Use the DataSource.FetchRange property to prevent frequent database lookups. When the SchedulerControl.VisibleIntervals value changes within the initially calculated Interval, the Interval value is not recalculated, and the Scheduler does not load additional data.

For example, the DataSource.FetchRange property is set to 6 days. The user selects two intervals to display in the Scheduler: Day3 and Day5-Day6. The event’s Interval property is Day2-Day7. When the user selects other days within the Day2-Day7 interval, the Scheduler does not load additional data.

Scheduler Items

Use the event’s Result property to specify the list of scheduler items to load from the data source. The Result property can accept data objects or unbound scheduler items.

Data Objects

To pass data objects to the Result property, set the DataSource.FetchMode property to Bound. Specify the AppointmentMappings/TimeRegionMappings property and map the Id. See the following help topic for more information: Mappings.

To load a recurrent pattern for the requested Interval, you also need to load all changed and deleted occurrences for this pattern. To correctly load recurrences, use the item’s QueryStart and QueryEnd properties.

Add two data source fields of the DateTime type in your data source. Specify the QueryStart and QueryEnd mappings for appointments/time regions to handle the DataSource.FetchAppointments/DataSource.FetchTimeRegions events. Scheduler sets and updates the values for these data properties.

The scheduler item’s QueryStart and QueryEnd properties allow you to calculate the correct interval that is used in a SELECT query when you handle the DataSource.FetchAppointments/DataSource.FetchTimeRegions events. The use of the TimeRegionMappings.Start and TimeRegionMappings.End properties is not recommended in this scenario because such an interval may not include time region patterns and the corresponding exceptions.

The example below illustrates how to fetch appointments from a DbContext source.

public class SchedulingDataContext : DbContext {
    public SchedulingDataContext() : base(CreateConnection(), true) { }
    static DbConnection CreateConnection() {
        //...
    }
    public DbSet<AppointmentEntity> AppointmentEntities { get; set; }

    //...
}
void dataSource_FetchAppointments(object sender, DevExpress.Xpf.Scheduling.FetchDataEventArgs e) {
    e.Result = dbContext.AppointmentEntities
        .Where(x => x.QueryStart <= e.Interval.End
            && x.QueryEnd >= e.Interval.Start)
        .ToArray();
}

The FetchDataEventArgs.GetFetchExpression method allows you to generate an expression to obtain appointments from the data source:

 public void FetchAppointments(FetchDataEventArgs args) {
    args.Result = dbContext.AppointmentEntities.Where(args.GetFetchExpression<AppointmentEntity>()).ToArray();
}

QueryStart/QueryEnd Initialization

The scheduler calculates and updates the QueryStart and QueryEnd values at runtime.

To initialize the QueryStart and QueryEnd values in your database, load all database records to the scheduler and then save changes back to the data source:

dbContext.AppointmentEntities.Load();
scheduler.DataSource.AppointmentsSource = dbContext.AppointmentEntities.Local;
dbContext.SaveChanges();

Unbound Items

If you do not use DataSource.AppointmentMappings and DataSource.TimeRegionMappings to bind the Scheduler to a data source, you need to load scheduler items (AppointmentItem and TimeRegionItem instances) instead of data items. To pass scheduler items to the Result property, set the DataSource.FetchMode property to Unbound.

 void FetchAppointments(object sender, FetchDataEventArgs e) {
    e.Result = MyCalendarService.GetEvents(e.Interval.Start, e.Interval.End).Select(Convert).ToArray();
}
AppointmentItem Convert(Event x) {
    var appt = new AppointmentItem() { Id = x.Id };
    //..
    return appt;
}

Save Changes

If you handle the DataSource.FetchAppointments event, you need to handle the following events to save the changes to the data source:

You can use the AppointmentCRUDEventArgs class to implement a single event handler for all four events:

 <dxsch:SchedulerControl
    AppointmentAdded="ProcessChanges"
    AppointmentEdited="ProcessChanges"
    AppointmentRemoved="ProcessChanges"
    AppointmentRestored="ProcessChanges"/>    
 void ProcessChanges(object sender, AppointmentCRUDEventArgs e) {
    db.Appointments.AddRange(e.AddToSource.Select(x => (Appointment)x.SourceObject));
    db.Appointments.RemoveRange(e.DeleteFromSource.Select(x => (Appointment)x.SourceObject));
    db.SaveChanges();
} 

One DbContext per Request

If you use the One DbContext per request approach, you need to initialize the DbContext in the DataSource.FetchAppointments event handler:

void FetchAppointments(object sender, FetchDataEventArgs e) {
    using(var db = new SchedulingContext()) {
        e.Result = db.Appointments.Where(e.GetFetchExpression<Appointment>()).ToArray();
    }
}

In this scenario, you need to use the event’s UpdateInSource property to save the changes to the existing appointments. You also need to search appointments by their Id to update or delete them:

 void ProcessChanges(object sender, AppointmentCRUDEventArgs e) {
    using(var db = new SchedulingContext()) {
        db.Appointments.AddRange(e.AddToSource.Select(x => (Appointment)x.SourceObject));
        foreach(var appt in e.UpdateInSource.Select(x => (Appointment)x.SourceObject))
            AppointmentHelper.CopyProperties(appt, db.Appointments.Find(appt.Id));
        foreach(var appt in e.DeleteFromSource.Select(x => (Appointment)x.SourceObject))
            db.Appointments.Remove(db.Appointments.Find(appt.Id));
        db.SaveChanges();
    }
}
public class AppointmentHelper {
    public static void CopyProperties(Appointment source, Appointment target) {
        target.AllDay = source.AllDay;
        target.AppointmentType = source.AppointmentType;
        target.Description = source.Description;
        target.End = source.End;
        target.Label = source.Label;
        target.Location = source.Location;
        target.QueryEnd = source.QueryEnd;
        target.QueryStart = source.QueryStart;
        target.RecurrenceInfo = source.RecurrenceInfo;
        target.ReminderInfo = source.ReminderInfo;
        target.ResourceId = source.ResourceId;
        target.Start = source.Start;
        target.Status = source.Status;
        target.Subject = source.Subject;
    }
}

Reload Data

The RefreshData() method clears the cached appointments/time regions and fires the DataSource.FetchAppointments and DataSource.FetchTimeRegions events.

Use the ReloadAppointments/ReloadTimeRegions methods to reload data. Use the event’s FetchDataEventArgs.Ids property to obtain the identifiers of the items that need to be updated in the Scheduler. If the fetch event has not been fired by the ReloadAppointments/ReloadTimeRegions methods, the FetchDataEventArgs.Ids property returns null.

The code snippet below illustrates the FetchAppointments event implementation.

scheduler.ReloadAppointment(new[] { id1, id2 });
//...
public void FetchAppointments(FetchDataEventArgs args) {
    if(args.Ids == null)
    // event has been fired by the scheduler's initialization or navigation
        args.Result = dbContext.AppointmentEntities
            .Where(x => x.QueryStart <= args.Interval.End && x.QueryEnd >= args.Interval.Start)
            .ToArray();
    else {
    // event has been fired by the ReloadAppointments method
        var ids = args.Ids.OfType<int>().ToArray();
        args.Result = dbContext.AppointmentEntities
            .Where((x) => ids.Contains(x.Id))
            .ToArray();
    }
}

Limitations

Example

View Example

This example consists of two solutions. Each solution has its own SchedulingViewModel:

  • CommonDbContext This example uses a single DbContext for the application.
  • DbContextPerRequest This example follows the One DbContext per request approach.

The Shared project contains the data and the view, which are the same for both solutions. The events are mapped to the view model’s handlers.

<dxsch:SchedulerControl x:Name="scheduler"
                        AppointmentAdded="{DXEvent 'ProcessChanges(@args)'}"
                        AppointmentEdited="{DXEvent 'ProcessChanges(@args)'}"
                        AppointmentRemoved="{DXEvent 'ProcessChanges(@args)'}"
                        AppointmentRestored="{DXEvent 'ProcessChanges(@args)'}"
                        >
    <dxsch:SchedulerControl.DataSource>
        <dxsch:DataSource FetchAppointments="{DXEvent 'FetchAppointments(@args)'}"
                            CreateSourceObject="{DXEvent 'CreateSourceObject(@args)'}"
                            ResourcesSource="{Binding Resources}"
                            Name="dataSource" FetchRange="1">
            <!--mappings-->
        </dxsch:DataSource>
    </dxsch:SchedulerControl.DataSource>
</dxsch:SchedulerControl>

Additionally, the view integrates the scheduler with the Date Navigator and Resource Navigator. The view displays the result of the fetch events in the Event Log text box.

CommonDbContext

The DbContext is initialized once the view model is initialized:

public class SchedulingViewModel {
    SchedulingContext dbContext;
    public SchedulingViewModel() {
        dbContext = new SchedulingContext();
        Resources = dbContext.ResourceEntities.ToList();
    }
    public virtual List<ResourceEntity> Resources { get; set; }
    public void CreateSourceObject(CreateSourceObjectEventArgs args) {
        if(args.ItemType == ItemType.AppointmentItem)
            args.Instance = new AppointmentEntity();
    }
    public void FetchAppointments(FetchDataEventArgs args) {
        args.Result = dbContext.AppointmentEntities.Where(args.GetFetchExpression<AppointmentEntity>()).ToArray();
    }
    public void ProcessChanges(AppointmentCRUDEventArgs args) {
        dbContext.AppointmentEntities.AddRange(args.AddToSource.Select(x => (AppointmentEntity)x.SourceObject));
        dbContext.AppointmentEntities.RemoveRange(args.DeleteFromSource.Select(x => (AppointmentEntity)x.SourceObject));
        dbContext.SaveChanges();
    }
}

DbContextPerRequest

The DbContext is initialized each time the DataSource.FetchAppointments event is fired.

public class SchedulingViewModel {
    public SchedulingViewModel() {
        using(var dbContext = new SchedulingContext()) {
            Resources = dbContext.ResourceEntities.ToList();
        }
    }
    public virtual List<ResourceEntity> Resources { get; set; }
    public void CreateSourceObject(CreateSourceObjectEventArgs args) {
        if(args.ItemType == ItemType.AppointmentItem)
            args.Instance = new AppointmentEntity();
    }
    public void FetchAppointments(FetchDataEventArgs args) {
        using(var dbContext = new SchedulingContext()) {
            args.Result = dbContext.AppointmentEntities.Where(args.GetFetchExpression<AppointmentEntity>()).ToArray();
        }
    }
        public void ProcessChanges(AppointmentCRUDEventArgs args) {
            using(var dbContext = new SchedulingContext()) {
                dbContext.AppointmentEntities.AddRange(args.AddToSource.Select(x => (AppointmentEntity)x.SourceObject));
                var updatedAppts = args.UpdateInSource.Select(x => (AppointmentEntity)x.SourceObject);
                var updatedApptIds = updatedAppts.Select(x => x.Id).ToArray();
                var apptsToUpdate = dbContext.AppointmentEntities.Where(x => updatedApptIds.Contains(x.Id));
                foreach(var appt in updatedAppts)
                    AppointmentEntityHelper.CopyProperties(appt, apptsToUpdate.First(x => x.Id == appt.Id));
                var deletedApptIds = args.DeleteFromSource.Select(x => ((AppointmentEntity)x.SourceObject).Id).ToArray();
                var apptsToDelete = dbContext.AppointmentEntities.Where(x => deletedApptIds.Contains(x.Id)).ToArray();
                dbContext.AppointmentEntities.RemoveRange(apptsToDelete);
                dbContext.SaveChanges();
            }
        }
}