Back to Devexpress

Load Data on Demand

wpf-402187-controls-and-libraries-scheduler-data-binding-load-data-on-demand.md

latest21.7 KB
Original Source

Load Data on Demand

  • Feb 06, 2024
  • 8 minutes to read

The SchedulerControl can load data on demand. You can write code so that the control only fetches the items that belong to the current visible interval. Use this technique to optimize initial load time and memory consumption if the Scheduler works with a large data source.

Run Demo: On-Demand Data Loading

View Example

To load items on demand, handle the following events:

AppointmentsDataSource.FetchAppointmentsTime RegionsDataSource.FetchTimeRegions

Use the FetchDataEventArgs.Result property to fetch data synchronously. Note that the application may freeze during the data fetch in case of a slow connection.

Use the FetchDataEventArgs.AsyncResult property to fetch data asynchronously. While the Scheduler loads data, a user can navigate to other time ranges, switch between Views, and interact with the control in other ways, except appointment changes. If the Scheduler needs time to load data, the Wait Indicator appears. To turn off the Wait Indicator during data load, set the ShowWaitIndicator property to false.

Time Interval

The SchedulerControl loads items based on the visible range. To load more items in advance, set your custom time interval in the DataSource.FetchRange property. In the DataSource.FetchAppointments and DataSource.FetchTimeRegions event handlers, you can use the FetchDataEventArgs.Interval property to get the time range for which the Scheduler loads items.

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 has the 6 days value. A user selects two intervals to display in the Scheduler: Day3 and Day5-Day6. The event’s Interval property is Day2-Day7. When a user selects other days within the Day2-Day7 interval, the Scheduler does not load additional data.

Scheduler Items

Use the FetchDataEventArgs.AsyncResult property to specify the list of Scheduler items to load from the data source. The AsyncResult property can accept data objects or unbound Scheduler items.

Data Objects

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

The FetchDataEventArgs.GetFetchExpression method allows you to generate an expression that obtains appointments from the data source. If the data source supports the IQueryable interface, use the following code sample to fetch data asynchronously:

csharp
public void FetchAppointments(FetchDataEventArgs args) {
    args.AsyncResult = dbContext.AppointmentEntities
        .Where(args.GetFetchExpression<AppointmentEntity>())
        .ToArrayAsync<object>();
}
vb
Public Sub FetchAppointments(ByVal args As FetchDataEventArgs)
    Dim res = dbContext.AppointmentEntities.Where(args.GetFetchExpression(Of AppointmentEntity)())
    args.AsyncResult = QueryableExtensions.ToArrayAsync(Of Object)(res)
End Sub

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

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

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.

csharp
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.AsyncResult = dbContext.AppointmentEntities
        .Where(x => x.QueryStart <= e.Interval.End
            && x.QueryEnd >= e.Interval.Start)
        .ToArrayAsync<object>();
}
vb
Public Class SchedulingDataContext
    Inherits DbContext

    Public Sub New()
        MyBase.New(CreateConnection(), True)
    End Sub
    Private Shared Function CreateConnection() As DbConnection
        '...
    End Function
    Public Property AppointmentEntities() As DbSet(Of AppointmentEntity)

    '...
End Class
Private Sub dataSource_FetchAppointments(ByVal sender As Object, ByVal e As DevExpress.Xpf.Scheduling.FetchDataEventArgs)
    Dim res = dbContext.AppointmentEntities.Where(Function(x) x.QueryStart <= e.Interval.End AndAlso x.QueryEnd >= e.Interval.Start)
    args.AsyncResult = QueryableExtensions.ToArrayAsync(Of Object)(res)
End Sub

Migrate Existing Projects to Load Data on Demand

You can migrate your project and use data loading on demand if your Scheduler is bound to a database. Perform the following step to implement data loading on demand:

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 to the data source:

csharp
dbContext.AppointmentEntities.Load();
scheduler.DataSource.AppointmentsSource = dbContext.AppointmentEntities.Local;
dbContext.SaveChanges();
vb
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, 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.

csharp
void FetchAppointments(object sender, FetchDataEventArgs e) {
    e.AsyncResult = MyCalendarService
        .GetEvents(e.Interval.Start, e.Interval.End)
        .ContinueWith(t => t.Result.Select(Convert).ToArray<object>());
}
AppointmentItem Convert(Event x) {
    var appt = new AppointmentItem() { Id = x.Id };
    //..
    return appt;
}
vb
Class SurroundingClass
    Private Sub FetchAppointments(ByVal sender As Object, ByVal e As FetchDataEventArgs)
        e.AsyncResult = MyCalendarService.GetEvents(e.Interval.Start, e.Interval.[End]).ContinueWith(Function(t) t.Result.[Select](AddressOf Convert).ToArray(Of Object)())
    End Sub

    Private Function Convert(ByVal x As [Event]) As AppointmentItem
        Dim appt = New AppointmentItem() With {
            .Id = x.Id
        }
        Return appt
    End Function
End Class

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:

xaml
<dxsch:SchedulerControl
    AppointmentAdded="ProcessChanges"
    AppointmentEdited="ProcessChanges"
    AppointmentRemoved="ProcessChanges"
    AppointmentRestored="ProcessChanges"/>
csharp
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();
}
vb
Private Sub ProcessChanges(ByVal sender As Object, ByVal e As AppointmentCRUDEventArgs)
    db.Appointments.AddRange(e.AddToSource.Select(Function(x) CType(x.SourceObject, Appointment)))
    db.Appointments.RemoveRange(e.DeleteFromSource.Select(Function(x) CType(x.SourceObject, Appointment)))
    db.SaveChanges()
End Sub

One DbContext per Request

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

csharp
void FetchAppointments(object sender, FetchDataEventArgs e) {
    using(var db = new SchedulingContext()) {
        e.AsyncResult = db.Appointments
            .Where(e.GetFetchExpression<Appointment>())
            .ToArrayAsync<object>();
    }
}
vb
Public Sub FetchAppointments(ByVal args As FetchDataEventArgs)
    Using dbContext = New SchedulingContext()
        Dim res = dbContext.AppointmentEntities.Where(args.GetFetchExpression(Of AppointmentEntity)())
        args.AsyncResult = QueryableExtensions.ToArrayAsync(Of Object)(res)
    End Using
End Sub

In this scenario, 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:

csharp
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;
    }
}
vb
Private Sub ProcessChanges(ByVal sender As Object, ByVal e As AppointmentCRUDEventArgs)
    Using db = New SchedulingContext()
        db.Appointments.AddRange(e.AddToSource.Select(Function(x) CType(x.SourceObject, Appointment)))
        For Each appt In e.UpdateInSource.Select(Function(x) CType(x.SourceObject, Appointment))
            AppointmentHelper.CopyProperties(appt, db.Appointments.Find(appt.Id))
        Next appt
        For Each appt In e.DeleteFromSource.Select(Function(x) CType(x.SourceObject, Appointment))
            db.Appointments.Remove(db.Appointments.Find(appt.Id))
        Next appt
        db.SaveChanges()
    End Using
End Sub
Public Class AppointmentHelper
    Public Shared Sub CopyProperties(ByVal source As Appointment, ByVal target As Appointment)
        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
    End Sub
End Class

Reload and Refresh Data

Use the ReloadAppointments/ReloadTimeRegions methods to reload specified appointments and time regions. 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.

csharp
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.AsyncResult = dbContext.AppointmentEntities
            .Where(x => x.QueryStart <= args.Interval.End && x.QueryEnd >= args.Interval.Start)
            .ToArrayAsync<object>();
    else {
    // event has been fired by the ReloadAppointments method
        var ids = args.Ids.OfType<int>().ToArray();
        args.AsyncResult = dbContext.AppointmentEntities
            .Where((x) => ids.Contains(x.Id))
            .ToArrayAsync<object>();
    }
}
vb
scheduler.ReloadAppointment( { id1, id2 })
'...
Public Sub FetchAppointments(ByVal args As FetchDataEventArgs)
    If args.Ids Is Nothing Then
    ' event has been fired by the scheduler's initialization or navigation
        args.AsyncResult = dbContext.AppointmentEntities
        .Where(Function(x) x.QueryStart <= args.Interval.End AndAlso x.QueryEnd >= args.Interval.Start)
        .ToArrayAsync(Of Object)()
    Else
    ' event has been fired by the ReloadAppointments method
        Dim ids = args.Ids.OfType(Of Integer)().ToArray()
        Dim res = dbContext.AppointmentEntities.Where(args.GetFetchExpression(Of AppointmentEntity)())
        args.AsyncResult = QueryableExtensions.ToArrayAsync(Of Object)(res)
    End If
End Sub

The RefreshData() method clears the cached appointments/time regions and fires the DataSource.FetchAppointments and DataSource.FetchTimeRegions events. You can use the CancellationToken property to cancel the data load.

To set up retries after an exception is thrown, you can use the AutoRetryFetchTimeOut and AutoRetryFetchMaxCount properties. When the AutoRetryFetchTimeOut or AutoRetryFetchMaxCount values are exceeded, the Retry window appears.

Limitations

Example

View Example

This example consists of three parts.

  • Shared. The project includes the data and the view that are the same for both examples.
  • CommonDbContext. The project uses a single DbContext for the application and has its own SchedulingViewModel.
  • DbContextPerRequest. This project follows the One DbContext per request approach and has its own SchedulingViewModel.