A simple read-only Sitecore data provider
An external data source can be integrated into Sitecore and represented as items by implementing a custom data provider. The custom data provider interacts with the external data source to define items, item hierarchies, handle actions and queries against those items. A custom data provider is built by inheriting from Sitecore's abstract DataProvider class and how much work the custom data provider should do depends on the problem you are addressing. You may only need to implement a data provider which overrides a single virtual method or possibly all of them.
One of the most basic forms of data provider is a read-only data provider and to demonstrate how easy data can be integrated into Sitecore we'll take a look at a very simple example, a data provider which takes some data from an in memory collection and transforms the data into items under a specific item in Sitecore's content tree.
This will involve the following steps:
- Create a Sitecore template which will be used for items generated by the data provider
- Create a root item in Sitecore to serve as a container for the items created by the data provider
- Create an in memory repository containing some data to integrate into Sitecore
- Create a custom data provider which will:
- Retrieve the data to be integrated into Sitecore
- Transform data into items in the Sitecore content tree
Sitecore templates
In this example we create two templates in Sitecore. One which will be used to create the root item for the data being integrated with the data provider and one for the actual items containing the data.
Data provider root item
Using the root item template an item is created in the Sitecore content tree which will be the parent item for our external data.
The external data
In this example the external data is coming from a simple in memory repository which simple provides a collections of objects.
01.
namespace
SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData
02.
{
03.
public
class
Rocket
04.
{
05.
public
string
ParentId {
get
;
set
; }
06.
public
string
RocketId {
get
;
set
; }
07.
public
string
Name {
get
;
set
; }
08.
public
string
Title {
get
;
set
; }
09.
public
string
Description {
get
;
set
; }
10.
}
11.
}
My in memory repository is simply returns a collection of rockets:
using
System.Collections.Generic;
namespace
SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData
{
public
class
RocketRepository
{
public
IEnumerable<Rocket> GetSimpleRocketCollection()
{
var rockets =
new
List<Rocket>()
{
new
Rocket()
{
ParentId =
null
,
RocketId =
"1001"
,
Name =
"Falcon 9"
,
Title =
"The Falcon 9"
,
Description =
"Falcon 9 is ...."
},
new
Rocket()
{
ParentId =
null
,
RocketId =
"1002"
,
Name =
"Titan"
,
Title =
"The Titan Rocket"
,
Description =
"Titan was ...."
}
};
return
rockets;
}
}
}
The read-only data provider
Create a new class
Create a class which inherits from Sitecore.Data.DataProviders.DataProvider
using
Sitecore.Data.DataProviders;
namespace
SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData
{
public
class
RocketReadOnlyDataProvider : DataProvider
{
}
}
Add a constructor
I would like to be able to configure the data provider using a config file which passes the database name, IDTablePrefix, rockets root item template an rocket template IDs so I create a constructor with some parameters and create an instance of the rocket repository:
using
System;
using
System.Linq;
using
Sitecore.Data;
using
Sitecore.Data.DataProviders;
using
Sitecore.Diagnostics;
using
Sitecore.Data.IDTables;
using
Sitecore.Data.Items;
using
System.Collections.Generic;
namespace
SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData
{
public
class
RocketReadOnlyDataProvider : DataProvider
{
private
readonly
string
_targetDatabaseName;
private
readonly
string
_idTablePrefix;
private
readonly
ID _rocketTemplateID;
private
readonly
ID _rocketRootTemplateID;
private
readonly
IEnumerable<Rocket> _rockets;
public
RocketReadOnlyDataProvider(
string
targetDatabaseName,
string
rocketRootTemplateID,
string
rocketTemplateID,
string
idTablePrefix)
{
Assert.ArgumentNotNullOrEmpty(targetDatabaseName,
"targetDatabaseName"
);
Assert.ArgumentNotNullOrEmpty(rocketRootTemplateID,
"rootTemplateId"
);
Assert.ArgumentNotNullOrEmpty(rocketTemplateID,
"simpleReadOnlyDataTemplateId"
);
Assert.ArgumentNotNullOrEmpty(idTablePrefix,
"idTablePrefix"
);
_targetDatabaseName = targetDatabaseName;
_idTablePrefix = idTablePrefix;
if
(!ID.TryParse(rocketRootTemplateID,
out
_rocketRootTemplateID))
throw
new
InvalidOperationException(
string
.Format(
"Invalid rocket root template ID {0}"
, rocketRootTemplateID));
if
(!ID.TryParse(rocketTemplateID,
out
_rocketTemplateID))
throw
new
InvalidOperationException(
string
.Format(
"Invalid rocket template ID {0}"
, rocketTemplateID));
_rockets =
new
RocketRepository().GetSimpleRocketCollection();
}
}
}
Override GetItemDefinition
This method is responsible for building an item definition based on the data to be integrated into Sitecore.
public
override
ItemDefinition GetItemDefinition(ID itemID, CallContext context)
{
Assert.ArgumentNotNull(itemID,
"itemID"
);
// Retrieve the rocket id from Sitecore's IDTable
var rocketId = GetRocketIdFromIDTable(itemID);
if
(!
string
.IsNullOrEmpty(rocketId))
{
// Retrieve the rocket data from the rockets collection
var rocket = _rockets.FirstOrDefault(o => o.RocketId == rocketId);
if
(rocket !=
null
)
{
// Ensure the rocket item name is valid for the Sitecore content tree
var itemName = ItemUtil.ProposeValidItemName(rocket.Name);
// Return a Sitecore item definition for the rocket using the rocket template
return
new
ItemDefinition(itemID, itemName, ID.Parse(_rocketTemplateID), ID.Null);
}
}
return
null
;
}
private
string
GetRocketIdFromIDTable(ID itemID)
{
var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemID);
if
(idTableEntries.Any())
return
idTableEntries[0].Key.ToString();
return
null
;
}
Override GetItemVersions
We need to provide some information which tells Sitecore which versions content is available in and in what languages. This is done by overriding the GetItemVersions method, here using just a little hack for sake of example.
public
override
VersionUriList GetItemVersions(ItemDefinition item, CallContext context)
{
if
(CanProcessItem(item.ID))
{
VersionUriList versions =
new
VersionUriList();
// Just a little hack
versions.Add(Language.Current, Sitecore.Data.Version.First);
context.Abort();
return
versions;
}
return
base
.GetItemVersions(item, context);
}
Override GetChildIDs
This method creates a list of item IDs representing the children of an item.
public
override
IDList GetChildIDs(ItemDefinition parentItem, CallContext context)
{
if
(CanProcessParent(parentItem.ID))
{
var itemIdList =
new
IDList();
foreach
(var rocket
in
_rockets)
{
var rocketId = rocket.RocketId;
// Retrieve the Sitecore item ID mapped to his rocket
IDTableEntry mappedID = IDTable.GetID(_idTablePrefix, rocketId);
if
(mappedID ==
null
)
{
// Map this rocket to a Sitecore item ID
mappedID = IDTable.GetNewID(_idTablePrefix, rocketId, parentItem.ID);
}
itemIdList.Add(mappedID.ID);
}
context.DataManager.Database.Caches.DataCache.Clear();
return
itemIdList;
}
return
base
.GetChildIDs(parentItem, context);
}
private
bool
CanProcessParent(ID id)
{
var item = Factory.GetDatabase(_targetDatabaseName).Items[id];
bool
canProcess =
false
;
if
(item.Paths.IsContentItem && item.TemplateID == _rocketRootTemplateID)
{
canProcess =
true
;
}
return
canProcess;
}
Override GetParentID
This method returns the parent id stored in the IDTable.
public
override
ID GetParentID(ItemDefinition itemDefinition, CallContext context)
{
var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemDefinition.ID);
if
(idTableEntries.Any())
{
return
idTableEntries.First().ParentID;
}
return
base
.GetParentID(itemDefinition, context);
}
Override GetItemFields
In order to populate the item fields with rocket data, we need to build a collection of item fields and map the rocket property values to these fields:
public
override
FieldList GetItemFields(ItemDefinition itemDefinition, VersionUri version, CallContext context)
{
var fields =
new
FieldList();
var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemDefinition.ID);
if
(idTableEntries.Any())
{
if
(context.DataManager.DataSource.ItemExists(itemDefinition.ID))
{
ReflectionUtil.CallMethod(
typeof
(ItemCache), CacheManager.GetItemCache(context.DataManager.Database),
"RemoveItem"
,
true
,
true
,
new
object
[] { itemDefinition.ID });
}
var template = TemplateManager.GetTemplate(_rocketTemplateID, Factory.GetDatabase(_targetDatabaseName));
if
(template !=
null
)
{
var rocketId = GetRocketIdFromIDTable(itemDefinition.ID);
if
(!
string
.IsNullOrEmpty(rocketId))
{
var rocket = _rockets.FirstOrDefault(o => o.RocketId == rocketId);
if
(rocket !=
null
)
{
foreach
(var field
in
GetDataFields(template))
{
fields.Add(field.ID, GetFieldValue(field, rocket));
}
}
}
}
}
return
fields;
}
protected
virtual
IEnumerable<TemplateField> GetDataFields(Template template)
{
return
template.GetFields().Where(ItemUtil.IsDataField);
}
private
string
GetFieldValue(TemplateField field, Rocket rocket)
{
string
fieldValue =
string
.Empty;
switch
(field.Name)
{
case
"Title"
:
fieldValue = rocket.Title;
break
;
case
"Description"
:
fieldValue = rocket.Description;
break
;
default
:
break
;
}
return
fieldValue;
}
Override GetLanguages(CallContext context)
Our simple data provider is only being used to grab some external data and integrate it into the Sitecore content tree but a data provider also has the responsibility of retrieving the languages content could be made available in. If we do not do anything to disable this in our data provider then our data provider will get the languages defined in Sitecore along with any other data providers configured. This results in duplicate languages appearing in the client.
To get around this we simply need to override the GetLanguages method in our data provider.
public
override
LanguageCollection GetLanguages(CallContext context)
{
return
null
;
}
And now the duplicate languages are gone.
Sitecore configuration
All that is left now is to register the new provider with Sitecore by creating a config file in the /App_Config/Include folder:
<
sitecore
>
<
dataProviders
>
<
rocketReadOnlyDataProvider
type
=
"SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData.RocketReadOnlyDataProvider, SitecoreCms.CodeSamples.DataProviders"
>
<
param
desc
=
"targetDatabaseName"
>master</
param
>
<
param
desc
=
"rocketRootTemplateID"
>{FBE22B26-1774-4F61-8DD4-434F691D0C8B}</
param
>
<
param
desc
=
"rocketTemplateID"
>{8DED8CEB-7140-4100-892D-52DD840FAF34}</
param
>
<
param
desc
=
"idTablePrefix"
>Rockets</
param
>
</
rocketReadOnlyDataProvider
>
</
dataProviders
>
<
databases
>
<
database
id
=
"master"
singleInstance
=
"true"
type
=
"Sitecore.Data.Database, Sitecore.Kernel"
>
<
dataProviders
hint
=
"list:AddDataProvider"
>
<
dataProvider
patch:before
=
"*[1]"
ref
=
"dataProviders/rocketReadOnlyDataProvider"
/>
</
dataProviders
>
</
database
>
</
databases
>
</
sitecore
>
</
configuration
>
Open up the Sitecore content editor to verify the data has been integrated into the content tree.
And that's it job done! Well not quite, this is a very simple example of how to integrate external data in Sitecore with a data provider and there is a lot more to address such as generating an hierarchical structure of items, editing external data from the Sitecore client, performance, caching and publishing.
Source code
You can find the source code for this simple data provider in the code sample repository and navigate to the RocketReadOnlyDataProvider class:
Sitecore7CodeSamples / SitecoreCms.CodeSamples.DataProviders / SimpleReadOnlyData / RocketReadOnlyDataProvider.cs
I am always fiddling around with the code so it may vary a little from the above but I am sure you'll get the idea.