We've seen in the past that some customers have had some difficulties in using XtraScheduler, and that this stems from misunderstandings about the scheduler's data and class model. This article is meant to help clarify some of the basic concepts of the objects and data structures used within XtraScheduler. It provides information on the hierarchy of objects and their interactions at different layers. This information will help you avoid some common implementation mistakes, those that are often caused by a lack of a general idea concerning the product's inner structure and behavior.
We'll start off with the scheduler's control model to gain some insight into which objects pertain to which layers in the suite. Then we'll discuss the technique of appointment inheritance and custom mapping that will enable you to modify the scheduler's inner structures. After that we'll present an overview of the scheduler's event model, especially with a description of the order in which events are raised.
Logical layers of the Scheduler
Internally, the Scheduler makes use of a set of layers to logically separate functionality. There are three logical layers: the data layer, the storage layer, and the control layer. Before an object, such as an appointment, is displayed within the Scheduler display area, it has to make its way through all these layers. At each layer, it gets transformed or wrapped into another object with its own specific attributes for that layer. These modifications are necessary, in particular for the case of recurring appointments, because their quantity and object hierarchy used do not correspond to the information contained within data source records.
Data Layer
The data layer contains a data source. Any object that implements the IList interface may become a data source for the Scheduler. Some of these objects are listed below:
- Simple objects, such as lists, arrays, or collections. It could be a list of user-defined classes, representing the Appointment business object.
- ADO .NET data objects - DataSet, DataTable with DataTableRow, DataView.
- XPO object collections
- ...
The layer contains data objects from the application's namespace.
When you use a Scheduler in its unbound mode, this layer is hidden from you. Appointment data manipulation (fetching, writing back, inserting, and deleting) is accomplished via an inner storage implementation.
Storage Layer
The storage layer stores collections of PersistentObjects. A PersistentObject is a proxy object automatically created for a business object when it is loaded from the data source. A PersistentObject has an internal property that serves to maintain a link (a row handle, if you will) to the data source in the data layer.
The layer contains Appointments from the DevExpress.XtraScheduler namespace.
Notice that you shouldn't mistake this layer for any storage mechanism you may have in the data layer. The storage layer here is temporary storage for the Scheduler itself.
Control Layer
The control layer contains the visual representations of the persistent objects from the previous layer. These visual objects are generated from the storage layer's data. The contents of the control layer are determined by the displayed time interval, the active Scheduler View, the control properties, and event handlers.
The appointments and resources collections in this layer include objects which have passed multiple filters on their way from the database, as well as objects that are newly created as the result of end-user actions (such as in-place editing, drag-n-drop, and so on).
The layer contains the following objects:
- collections of appointments (AppointmentViewInfoCollection)
- collections of resource containers (resource headers, day view columns, single weeks, timelines)
They belong to the DevExpress.XtraScheduler.Drawing namespace.
Layer Interaction
The following picture summarizes the layer interactions within XtraScheduler.
Data Layer - Storage Layer
A simple mapping technique is used for establishing the communication between the data and storage layers. The mapping describes how the PersistentObject's properties contained within the storage layer correspond to the data fields at the data layer.
In unbound mode, the data layer is hidden, and the property mapping is undefined.
The storage layer establishes a two-way communication with data source objects, which means that any addition or modification to storage objects results in the equivalent actions being performed on the data layer objects, and vice versa.
Storage Layer - Control Layer
The control layer interacts only with the storage layer objects. It has no direct contact with the data layer objects.
The addition, modification, or deletion of control layer objects results in the corresponding changes being applied to the storage objects, and vice versa.
The Storage layer has an appointment cache, populated with appointments that satisfy the currently displayed visible interval. This is the part of the working area of the Scheduler control visible to the end-user. When the Scheduler control changes its view and the visible interval is not modified, the control fetches the appointments from the cache, helping to optimize the query route and reducing the time to refresh the display area.
It is worth mentioning here that the DateNavigator control also has its own appointment cache, which is used to highlight dates with appointments. This cache is different from the one we just described for the storage layer and has no connection with it.
Object Interaction
The interactions of the various objects defined in the Scheduler are illustrated in the following image.
Storage Layer objects – Control Layer objects
A single PersistentObject can produce several views, for example in the case when an appointment occupies more than one day and is displayed split apart, or when an appointment does not belong to a specific resource and is displayed for each resource individually.
Every AppointmentViewInfo object belongs to a single Appointment, and this appointment object is accessible via the Appointment property. You should use this property whenever it is necessary to get the related appointment for the visual object. This usually happens within Custom Draw event handlers, such as when you process mouse clicks to determine which object was clicked (hit testing) and so on.
Data Layer objects – Storage Layer objects
PersistentObjects at the storage layer are created from information provided by data objects in the data layer. The storage loads data according to the specified requirements.
A PersistentObject is created for each data object obtained from the data source. Not every PersistentObject so constructed is included into the storage collection, however: recurrence exceptions are created along with other persistent objects and stored, not in the PersistentObjectCollection, but separately in the Exceptions collection of the Pattern object.
If a PersistentObject is created from a data object, it contains a link to its data source record (the row). The corresponding object at the Data layer can be obtained via the PersistentObject.GetSourceObject(SchedulerStorageBase storage) method. The scope of this method is limited to persistent objects originating from data objects and not for the appointment occurrences and newly created appointments.
There are certain scenarios in which persistent objects are created by replicating existing instances. It may be by the end-user interactively editing the PersistentObject's visual representation, or by them dragging and dropping an appointment to create a new one. These objects are not linked to any data layer item and the GetSourceObject method is inapplicable.
The following storage layer methods are useful for a direct manipulation with linked objects at the Data layer:
public object GetObjectRow(PersistentObject obj)
public object GetObjectValue(PersistentObject obj, string columnName)
public void SetObjectValue(PersistentObject obj, string columnName, object val)
Extending the PersistentObject functionality
Custom Fields and Mappings
You have the ability to define custom data fields and map them to custom properties of persistent objects. This mechanism enhances your ability to control actions performed with PersistentObject instances. You can use these user-defined property values within your event handlers to select items for display, to modify their visual representation, and so on.
Inheritance
You can also implement appointment inheritance, by creating descendant classes via the IAppointmentFactory interface of the AppointmentStorage class. You can register your own appointment class factory by means of the AppointmentStorage.SetAppointmentFactory(IAppointmentFactory factory) method.
This technique helps you extend the set of properties of an appointment object. Of course, you are then responsible for the correct realization of this extra functionality.
Handling Data-Related Events
Persistent objects publish two categories of data-related events.
The first set includes events that are raised before the action takes place. They are usually named with the "ing" ending, and are used for pre-processing and cancelling the action. An event handler in this case receives one object, which already has been modified. If you set the Cancel parameter to true, the modification is rolled back.
Several events do not enable you to get the object in the unmodified state, so you will have to query the corresponding data object directly to obtain property values before modification.
Then there are events in this category that receive both the original and the changed objects. For example, the AppointmentDrag event receives both the EditedAppointment and SourceAppointment objects. You should clearly distinguish these two objects when handling this event.
The second set of events includes events with the "ed" ending, which are fired when the action has been performed. They serve for notification purposes and post-processing.
The two events that correspond to one action and that belong to different categories form an event pair. Events for PersistentObject collections, in contrast to single objects, do not come in pairs.
Guidelines for correct event handling are listed below.
- A good practice is to use BeginUpdate() / EndUpdate() blocks, because it reduces time required for the initialization and prevents the excessive raising of events. It is especially effective when event handling takes a considerable amount of time.
- Do not remove the object instance for which the first event in a pair is fired (the "ing" event). Its properties can be changed within the BeginUpdate() / EndUpdate() block. Deleting an object in the middle of this action, in between events, can result in some hard-to-find errors.
- Pay special attention to explicit Storage.RefreshData calls. Data will be reloaded, and all custom field values that are not mapped will be lost. In particular, this may happen when a Scheduler Storage operates in unbound mode.
- When handling the FetchAppointments event, change data in the data source at the Data layer. Do not modify data at the Storage layer, because the Storage automatically reloads its data from the Data layer's data source when this event handler is executed.
- When the data source is changed, and the AutoReload property is set to true, the AutoReloading event occurs, and the Storage starts an automatic refresh. Use the AutoReloading event as a notification.
The order of events raised in typical situations is illustrated in the following table:
|
Action |
Event |
Description |
|
Scheduler View initialization |
AppointmentCollectionAutoReloading AppointmentCollectionLoaded FetchAppointments FilterAppointments* |
|
|
Visible interval modification of a View or a Control |
FetchAppointments Filter Appointments* |
|
|
Clearing the PersistentObjectStorage |
AppointmentCollectionCleared |
|
|
PersistentObjectStorage.DataSource substitution |
AppointmentCollectionLoaded |
|
|
Adding a new appointment to a collection |
AppointmentInserting AppointmentsInserted FilterAppointments* |
|
|
Occurrence appointments modification, resulting in exceptions for a particular recurrence pattern. |
AppointmentChanging AppointmentsInserted AppointmentsChanged FilterAppointments
|
The AppointmentsInserted event is raised for the newly created Changed Occurrence. |
|
Occurrence appointments removal, resulting in exceptions for a particular recurrence pattern. |
AppointmentDeleting AppointmentsInserted FilterAppointments* |
The AppointmentsInserted event is raised for the newly created Deleted Occurrence. |
|
Removing a pattern with two exceptions |
AppointmentDeleting AppointmentDeleting AppointmentDeleting AppointmentsDeleted FilterAppointments* |
The AppointmentDeleting event is first raised for the pattern and then - for all its exceptions. Note: if deleting of an exception is cancelled, the entire process is cancelled, and nothing is deleted. |
|
Modification of three appointments at once with an action cancelled for one of them (e.g. drag-move) |
AppointmentChanging AppointmentChanging AppointmentChanging AppointmentsChanged |
The AppointmentChanging event is raised three times (one for each appointment). The action is cancelled in one of them. In the AppointmentChanged event, we have only two modified appointments. |
* If the filter is applied, the FilterAppointments event is raised for each appointment that falls within the visible interval.