Implementing a Provider
To implement a RadScheduler provider, you create a class that inherits from SchedulerProviderBase.It must implement the GetAppointments, Insert, Update, Delete, GetResourceTypes and GetResourcesByType methods.
The provider is instantiated once per application domain and is shared across threads. RadScheduler ensures basic thread safety by encapsulating each provider in a wrapper that provides locks around each of its public methods.
This example shows how to create the XML-based provider that is already included in the Telerik.Web.UI assembly, along with RadScheduler itself. It is a lightweight and easy to deploy alternative to using a full-blown database. The provider uses an XML file to store information about each appointment. The XML file also contains information about the custom resources and some implementation-specific details such as the next appointment identity key. The XML file looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Appointments>
<NextID>3</NextID>
<Resources>
<Room>
<Key>1</Key>
<Text>Meeting room 101</Text>
</Room>
<Room>
<Key>2</Key>
<Text>Meeting room 201</Text>
</Room>
<User>
<Key>1</Key>
<Text>Alex</Text>
</User>
<User>
<Key>2</Key>
<Text>Bob</Text>
</User>
<User>
<Key>3</Key>
<Text>Charlie</Text>
</User>
</Resources>
<Appointment>
<ID>1</ID>
<Subject>Technical meeting</Subject>
<Start>2007-03-30T06:00Z</Start>
<End>2007-03-30T07:00Z</End>
<RecurrenceRule>
<![CDATA[
DTSTART:20070330T060000Z
DTEND:20070330T070000Z
RRULE:FREQ=DAILY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;
]]>
</RecurrenceRule>
<Resources>
<Room Key="1" />
<User Key="1" />
</Resources>
<Attribute Key="CustomAttribute" Value="1" />
</Appointment>
<Appointment>
<ID>2</ID>
<Subject>Lunch</Subject>
<Start>2007-03-30T09:00Z</Start>
<End>2007-03-30T10:00Z</End>
<Resources>
<User Key="1" />
</Resources>
</Appointment>
</Appointments>
- The new provider class is called MyXmlSchedulerProvider, to distinguish it from the provider in the Telerik.Web.UI assembly that we are copying. It must be derived from SchedulerProviderBase. In order to implement it, we need to include a number of assemblies, including Telerik.Web.UI, System.Xml, System.IO, System.Configuration.Provider, System.Collections.Specialized, and System.Collections.Generic.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.IO;
using System.Xml;
using Telerik.Web.UI;
namespace MyNamespace
{
public class MyXmlSchedulerProvider : SchedulerProviderBase
{
}
}
- Add to the provider some private fields for holding basic information. These include an XmlDocument instance for manipulating the XML data file, a list of resources, and basic state and configuration information:
private const string DateFormatString = "yyyy-MM-ddTHH:mmZ";
private string _dataFileName;
private int _nextID;
private XmlDocument _doc;
private List<Resource> _resources;
private bool _persistChanges;
private bool _documentLoaded;
- Before implementing the constructors, we need two helper functions for reading from the XML document.ReadNextID() looks up the next appointment ID.
LoadResources() loads the resource data from the XML document.
private int ReadNextID()
{
return int.Parse(_doc.SelectSingleNode("//Appointments/NextID").InnerText);
}
private void LoadResources()
{
_resources = new List<Resource>();
foreach (XmlNode resourcesNode in _doc.SelectNodes("//Appointments/Resources"))
{
foreach (XmlNode resourceNode in resourcesNode.ChildNodes)
{
Resource resource = new Resource();
_resources.Add(resource);
resource.Type = resourceNode.Name;
foreach (XmlNode resourceData in resourceNode.ChildNodes)
{
switch (resourceData.Name)
{
case "Key":
resource.Key = resourceData.InnerText;
break;
case "Text":
resource.Text = resourceData.InnerText;
break;
}
}
}
}
}
- The constructor initializes the global fields to save the XML document that it uses to manipulate its data, the first ID to use for an appointment,the persistChanges flag, and a boolean indicating that the XML document has been loaded. For added flexibility, the providerimplements two additional constructors that allow it to be configured at run-time. They allow using a XmlDocument instance or a file as a data store:
public XmlSchedulerProvider()
{
_doc = new XmlDocument();
_nextID = 1;
_persistChanges = false;
_documentLoaded = true;
}
public XmlSchedulerProvider(string dataFileName, bool persistChanges)
{
_dataFileName = dataFileName;
_doc = new XmlDocument();
_doc.Load(_dataFileName);
_nextID = ReadNextID();
LoadResources();
_persistChanges = persistChanges;
_documentLoaded = true;
}
public XmlSchedulerProvider(XmlDocument doc)
{
_doc = doc;
_nextID = ReadNextID();
LoadResources();
_persistChanges = false;
_documentLoaded = true;
}
- In order to support declarative configuration by a Web.config section, the provider overrides the Initialize method.After checking for valid parameters, Initialize calls its base implementation and uses the config collectionto read the configuration values. The "persistChanges" attribute defaults to "true" if not specified. The actual loading of the documentis deferred for performance reasons.
public override void Initialize(string name, NameValueCollection config)
{
if (config == null)
{
throw new ArgumentNullException("config");
}
if (string.IsNullOrEmpty(name))
{
name = "XmlSchedulerProvider";
}
base.Initialize(name, config);
_dataFileName = config["fileName"];
if (string.IsNullOrEmpty(_dataFileName))
{
throw new ProviderException("Missing XML data file name. Please specify it with the fileName property.");
}
string persistChanges = config["persistChanges"];
if (!string.IsNullOrEmpty(persistChanges))
{
if (!bool.TryParse(persistChanges, out _persistChanges))
{
throw new ProviderException("Invalid value for PersistChanges attribute. Use 'True' or 'False'.");
}
}
else
{
_persistChanges = true;
}
_documentLoaded = false;
}
- A provider must implement the GetAppointments, Insert,Update, Delete, GetResourceTypes, andGetResourcesByTypes methods. However, before implementing these methods, we add some more helper functions. SaveAppointmentResources adds the resources for an appointment to the XML node for that appointment. SaveAppointmentAttributes adds any custom attributes for the appointment to its XML node.CreateAppointmentNode adds a node to the XML document that gets its data from an Appointment object.LoadDataFile checks whether the XML document needs to be loaded from the file, and if so, loads it and initializesthe _nextID field. SaveDataFile saves the XML document to the associated file.EnsureFilePath calls Page.MapPath to resolve the XML file name.
private void SaveAppointmentResources(Appointment appointment, XmlNode appointmentNode)
{
if (appointment.Resources.Count == 0)
{
return;
}
XmlNode resourcesGroupNode = _doc.CreateNode(XmlNodeType.Element, "Resources", string.Empty);
appointmentNode.AppendChild(resourcesGroupNode);
foreach (Resource res in appointment.Resources)
{
XmlNode resourceNode = _doc.CreateNode(XmlNodeType.Element, res.Type, string.Empty);
resourcesGroupNode.AppendChild(resourceNode);
XmlAttribute keyAttribute = _doc.CreateAttribute("Key");
resourceNode.Attributes.Append(keyAttribute);
keyAttribute.InnerText = res.Key.ToString();
}
}
private void SaveAppointmentAttributes(Appointment appointment, XmlNode appointmentNode)
{
foreach (string attribute in appointment.Attributes.Keys)
{
if (appointment.Attributes[attribute] != string.Empty)
{
XmlNode attributeNode = _doc.CreateNode(XmlNodeType.Element, "Attribute", string.Empty);
appointmentNode.AppendChild(attributeNode);
XmlAttribute keyAttribute = _doc.CreateAttribute("Key");
attributeNode.Attributes.Append(keyAttribute);
keyAttribute.InnerText = attribute;
XmlAttribute valueAttribute = _doc.CreateAttribute("Value");
attributeNode.Attributes.Append(valueAttribute);
valueAttribute.InnerText = appointment.Attributes[attribute];
}
}
}
private XmlNode CreateAppointmentNode(Appointment appointment)
{
XmlNode appointmentNode = _doc.CreateNode(XmlNodeType.Element, "Appointment", string.Empty);
XmlNode appointmentID = _doc.CreateNode(XmlNodeType.Element, "ID", string.Empty);
appointmentID.InnerText = appointment.ID.ToString();
appointmentNode.AppendChild(appointmentID);
XmlNode appointmentSubject = _doc.CreateNode(XmlNodeType.Element, "Subject", string.Empty);
appointmentSubject.InnerText = appointment.Subject;
appointmentNode.AppendChild(appointmentSubject);
XmlNode appointmentStart = _doc.CreateNode(XmlNodeType.Element, "Start", string.Empty);
appointmentStart.InnerText = appointment.Start.ToUniversalTime().ToString(DateFormatString);
appointmentNode.AppendChild(appointmentStart);
XmlNode appointmentEnd = _doc.CreateNode(XmlNodeType.Element, "End", string.Empty);
appointmentEnd.InnerText = appointment.End.ToUniversalTime().ToString(DateFormatString);
appointmentNode.AppendChild(appointmentEnd);
if (!string.IsNullOrEmpty(appointment.RecurrenceRule))
{
XmlNode appointmentRecurrenceRule = _doc.CreateNode(XmlNodeType.Element, "RecurrenceRule", string.Empty);
appointmentNode.AppendChild(appointmentRecurrenceRule);
XmlNode recurrenceRuleCdata = _doc.CreateNode(XmlNodeType.CDATA, string.Empty, string.Empty);
appointmentRecurrenceRule.AppendChild(recurrenceRuleCdata);
recurrenceRuleCdata.InnerText = appointment.RecurrenceRule;
}
if (appointment.RecurrenceState == RecurrenceState.Exception)
{
XmlNode appointmentRecurrenceParentID = _doc.CreateNode(XmlNodeType.Element, "RecurrenceParentID", string.Empty);
appointmentRecurrenceParentID.InnerText = appointment.RecurrenceParentID.ToString();
appointmentNode.AppendChild(appointmentRecurrenceParentID);
}
SaveAppointmentResources(appointment, appointmentNode);
SaveAppointmentAttributes(appointment, appointmentNode);
return appointmentNode;
}
private void LoadDataFile()
{
if (string.IsNullOrEmpty(_dataFileName) || _documentLoaded)
{
return;
}
_doc.Load(_dataFileName);
_nextID = ReadNextID();
LoadResources();
_documentLoaded = true;
}
private void SaveDataFile()
{
if (_persistChanges && !string.IsNullOrEmpty(_dataFileName))
{
_doc.Save(_dataFileName);
}
}
private void EnsureFilePath(RadScheduler owner)
{
if ((owner.Page == null) || File.Exists(_dataFileName))
{
return;
}
_dataFileName = owner.Page.MapPath(_dataFileName);
}
- The provider must implement GetAppointments to provide the scheduler with the appointment data currentlystored in the XML document. GetAppointments reads the appointment nodes from the XML file, and for each one,generates an Appointment object. These Appointment objects are added to a list ofappointments, which GetAppointments returns to the scheduler.
public override IEnumerable<Appointment> GetAppointments(RadScheduler owner)
{
EnsureFilePath(owner);
LoadDataFile();
List<Appointment> appointmentsList = new List<Appointment>();
foreach (XmlNode appointmentNode in _doc.SelectNodes("//Appointments/Appointment"))
{
Appointment appointment = new Appointment();
appointmentsList.Add(appointment);
foreach (XmlNode appointmentData in appointmentNode.ChildNodes)
{
switch (appointmentData.Name)
{
case "ID":
appointment.ID = int.Parse(appointmentData.InnerText);
break;
case "Subject":
appointment.Subject = appointmentData.InnerText;
break;
case "Start":
appointment.Start = DateTime.Parse(appointmentData.InnerText);
break;
case "End":
appointment.End = DateTime.Parse(appointmentData.InnerText);
break;
case "RecurrenceRule":
appointment.RecurrenceRule = appointmentData.InnerText;
appointment.RecurrenceState = RecurrenceState.Master;
break;
case "RecurrenceParentID":
appointment.RecurrenceParentID = int.Parse(appointmentData.InnerText);
appointment.RecurrenceState = RecurrenceState.Exception;
break;
case "Resources":
LoadAppointmentResources(owner, appointment, appointmentData);
break;
case "Attribute":
appointment.Attributes.Add(
appointmentData.Attributes[ "Key"].Value,
appointmentData.Attributes["Value"].Value);
break;
}
}
}
return appointmentsList;
}
- The provider must implement an Insert method to add appointments to the XML document. Insert assigns an ID to the new appointment, using the _nextID global field, and savesa new value for _nextID in the XML document as well as the data for the new appointment:
public override void Insert(RadScheduler owner, Appointment appointmentToInsert)
{
appointmentToInsert.ID = _nextID;
XmlNode appointmentsNode = _doc.SelectSingleNode("//Appointments");
appointmentsNode.AppendChild(CreateAppointmentNode(appointmentToInsert));
_nextID++;
_doc.SelectSingleNode("//Appointments/NextID").InnerText = _nextID.ToString();
SaveDataFile();
}
- The provider must implement the Update method to apply changes from the scheduler:
public override void Update(RadScheduler owner, Appointment appointmentToUpdate)
{
if (appointmentToUpdate.ID == null)
{
Insert(owner, appointmentToUpdate);
}
XmlNode appointmentNode = _doc.SelectSingleNode("//Appointments/Appointment[ID=" + appointmentToUpdate.ID + "]");
appointmentNode.ParentNode.ReplaceChild(CreateAppointmentNode(appointmentToUpdate), appointmentNode);
SaveDataFile();
}
- The provider implements the Delete method to delete appointments from the XML document:
public override void Delete(RadScheduler owner, Appointment appointmentToDelete)
{
XmlNode appointmentNode = _doc.SelectSingleNode("//Appointments/Appointment[ID=" + appointmentToDelete.ID + "]");
appointmentNode.ParentNode.RemoveChild(appointmentNode);
SaveDataFile();
}
- The provider implements the GetResourceTypes method to tell the scheduler what custom resourcesare available. Note that is only needs to specify the names of the types:
public override IEnumerable<ResourceType> GetResourceTypes(RadScheduler owner)
{
EnsureFilePath(owner);
LoadDataFile();
List<string> resourceTypeNames = new List<string>();
foreach (Resource res in _resources)
{
if (!resourceTypeNames.Contains(res.Type))
{
resourceTypeNames.Add(res.Type);
}
}
List<ResourceType> resourceTypes = new List<ResourceType>();
foreach (string resourceTypeName in resourceTypeNames)
{
resourceTypes.Add(new ResourceType(resourceTypeName));
}
return resourceTypes;
}
- The provider implements GetResourcesByType to supply the possible values for a specific resource type:
public override IEnumerable<Resource> GetResourcesByType(RadScheduler owner, string resourceType)
{
EnsureFilePath(owner);
LoadDataFile();
return _resources.FindAll(delegate(Resource res)
{
return res.Type == resourceType;
});
}
In addition you can find a full sample project for "Web Services with Custom Provider" by adding a Scenario Template. Follow these steps to add the scenario:
-
Right-click on the Web site name in Solution Explorer window. Select "RadControls for ASP.NET AJAX". From the submenu choose"Add RadScheduler Scenario".
-
Scenario Wizard appears with different scenarios. Choose "Web Service with Custom Provider":
-
Follow the wizard by pressing**"Next"** button and finally press "Finish". A new .aspx page will be added to your project, depending on your choice in the Scenario Wizard. All necessary references will be added to your project.
-
Press Ctrl+F5 and run the application.