wpf-402187-controls-and-libraries-scheduler-data-binding-load-data-on-demand.md
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
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.
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.
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.
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:
public void FetchAppointments(FetchDataEventArgs args) {
args.AsyncResult = dbContext.AppointmentEntities
.Where(args.GetFetchExpression<AppointmentEntity>())
.ToArrayAsync<object>();
}
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.
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>();
}
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
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:
dbContext.AppointmentEntities.Load();
scheduler.DataSource.AppointmentsSource = dbContext.AppointmentEntities.Local;
dbContext.SaveChanges();
dbContext.AppointmentEntities.Load()
scheduler.DataSource.AppointmentsSource = dbContext.AppointmentEntities.Local
dbContext.SaveChanges()
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.
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;
}
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
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();
}
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
If you use the One DbContext per request approach, initialize the DbContext in the DataSource.FetchAppointments event handler:
void FetchAppointments(object sender, FetchDataEventArgs e) {
using(var db = new SchedulingContext()) {
e.AsyncResult = db.Appointments
.Where(e.GetFetchExpression<Appointment>())
.ToArrayAsync<object>();
}
}
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:
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;
}
}
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
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.
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>();
}
}
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.
This example consists of three parts.
SchedulingViewModel.SchedulingViewModel.