In Sitefinity everyone is using RelatedData and usually we want to display the related data items on the front-end. In this blog post I will describe how you can create a single control that will resolve an item related data fields and show the data items. The item is resolved using Url parameters and the control is intended to work with the out of the box Sitefinity widgets. When the widget resolve an item in a detail view, the related data control will resolve its fields and items related. This way you do not have to modify the items’ widget template to show the related items and do not need to define different controls for each field.
The widget provides an option to show the items using either simple links or items widget. This is configured through the control designer.
Our control inherits from ContentView and contains only a DetailView. This way we stick to the original built-in controls and separate the logic between the main control and the views.
In the post will be also shown how you can get a manager and resolve an item using the type of the content and how to cache already resolved item to prevent unnecessary querying it again.
Getting the item from Url
We will use the ContentView ResolveDetailItem method to resolve the item by overriding it. We first try to resolve a dynamic item using the Url parameters. We will check the item locations, so we are sure this is the item resolved on the current page. This is necessary because there could be items from different types with equal urls.
internal
static
IDataItem TryGetDynamicItemFromUrl(
string
urlParams)
{
var typeStr =
typeof
(DynamicContent).FullName;
Type typeCurrent = TypeResolutionService.ResolveType(typeStr);
var items = DynamicModuleManager.GetManager().GetDataItems(typeCurrent)
.Where(i => i.ItemDefaultUrl == urlParams && i.Status == ContentLifecycleStatus.Live);
var count = items.Count();
if
(count > 1)
{
foreach
(var item
in
items)
{
var service = Telerik.Sitefinity.Services.SystemManager.GetContentLocationService();
var isThisItem = service.GetItemLocations(item).Any(s => s.PageId ==
new
Guid(SiteMap.CurrentNode.Key));
if
(isThisItem)
{
return
item;
}
}
}
else
{
return
items.FirstOrDefault();
}
return
null
;
}
Using the default content items type, we will try to resolve an item using the items type manager.
internal
static
IDataItem TryGetContentItemFromUrl(
string
urlParams)
{
var typeNames = RelatedDataControlUrlHelper.Types;
foreach
(var itemType
in
typeNames)
{
var typeString = itemType;
Type type = TypeResolutionService.ResolveType(typeString);
var manager = ManagerBase.GetMappedManager(typeString);
string
redirectUrl =
string
.Empty;
var currentItem = ((IContentManager)manager).GetItemFromUrl(type, urlParams,
out
redirectUrl);
if
(currentItem !=
null
)
{
return
currentItem;
}
}
return
null
;
}
We will cache the item resolved, using the Url parameters and the page key:
protected
override
void
SubscribeCacheDependency()
{
if
(
this
.DetailItem !=
null
)
{
string
key = ConstructCacheKey();
var inCache =
this
.CacheManager.Contains(key);
var item =
this
.DetailItem;
if
(!inCache)
{
this
.CacheManager.Add(key,
item,
CacheItemPriority.Normal,
null
,
new
DataItemCacheDependency(item.GetType(), item.Id),
new
SlidingTime(TimeSpan.FromMinutes(30)));
}
}
}
protected
virtual
string
ConstructCacheKey()
{
var pageId = SiteMap.CurrentNode.Key;
if
(!
string
.IsNullOrEmpty(
this
.urlParameters) && !
string
.IsNullOrEmpty(pageId))
{
var key =
string
.Format(
"{0}_{1}"
,
this
.urlParameters, pageId);
return
key;
}
return
null
;
}
Get the Related data fields
Getting the related data fields is also split on dynamic type items and content items. In the first case we will use the ModuleBuilderManager and in the second we will get the fields which are RelatedDataPropertyDescriptor type.
protected
virtual
void
ProcessDynamicItem(DynamicContent item)
{
ModuleBuilderManager moduleBuilderManager = ModuleBuilderManager.GetManager();
var type = moduleBuilderManager.GetItems(
typeof
(DynamicModuleType), String.Empty, String.Empty, 0, 0)
.OfType<DynamicModuleType>()
.FirstOrDefault(dmt => item.GetType().FullName.Equals(dmt.GetFullTypeName()));
var mainShortTextField = type.MainShortTextFieldName;
var fields = moduleBuilderManager.Provider.GetDynamicModuleFields()
.Where(f => f.ParentTypeId == type.Id && (f.FieldType == FieldType.RelatedData || f.FieldType == FieldType.RelatedMedia));
var itemType = item.GetType();
GetRelatedData(item, itemType, fields);
}
var item = currentItem
as
IDynamicFieldsContainer;
if
(item !=
null
)
{
var fields = TypeDescriptor.GetProperties(type).OfType<RelatedDataPropertyDescriptor>();
var models =
new
List<Model>();
foreach
(var field
in
fields)
{
var current =
new
Model();
current.Item = item;
current.FieldName = field.Name;
var attributesCollection = field.Attributes[
typeof
(MetaFieldAttributeAttribute)]
as
MetaFieldAttributeAttribute;
if
(attributesCollection !=
null
)
{
string
childItemTypeName =
null
;
string
childItemProviderName =
null
;
attributesCollection.Attributes.TryGetValue(
"RelatedType"
,
out
childItemTypeName);
attributesCollection.Attributes.TryGetValue(
"RelatedProviders"
,
out
childItemProviderName);
current.FieldType = childItemTypeName;
current.RelatedDataProvider = childItemProviderName;
}
models.Add(current);
}
}
Get the related items - when using simple links
We query the related items data and filter using the parent item status, so we get only single version of an item. It is needed to query a cached item once again using its id, since the cached one is out of the object scope and it is not able to get the related items.
// Get the related items - when using simple links.
// Return IEnumerable object - common DataSource
public
IEnumerable<
object
> GetRelatedItems(
object
item)
{
var it = item
as
Model;
if
(
this
.isItemFromCache)
{
// Query the item, since the cached one is not in the object scope
if
(cachedItem ==
null
)
{
var type = it.Item.GetType();
var manager = ManagerBase.GetMappedManager(type);
var attachedItem = manager.GetItem(type, it.Item.GetValue<Guid>(
"Id"
));
this
.cachedItem = attachedItem
as
IDataItem;
}
// filter the related items by the parent item status (or both master/live will be queried)
ContentLifecycleStatus status = (
this
.cachedItem
as
ILifecycleDataItemGeneric).Status;
var result =
this
.cachedItem.GetRelatedItems(it.FieldName).ToList().OfType<ILifecycleDataItemGeneric>()
.Where(i => i.Status == status);
return
result;
}
else
{
ContentLifecycleStatus status = it.Item.GetValue<ContentLifecycleStatus>(
"Status"
);
var result = it.Item.GetRelatedItems(it.FieldName).ToList().OfType<ILifecycleDataItemGeneric>()
.Where(i => i.Status == status);
return
result;
}
}
Using default widgets to show related items
Using the RelatedDataDefinition in the default widgets, we will show the related items:
public
static
class
RelatedDataControlExtensions
{
internal
static
void
SetRelatedDataDefinitionProperties(
this
IRelatedDataView control, Model dataItem)
{
control.RelatedDataDefinition.RelatedFieldName = dataItem.FieldName;
control.RelatedDataDefinition.RelatedItemType = dataItem.Item.GetType().FullName;
control.RelatedDataDefinition.RelationTypeToDisplay = RelationDirection.Child;
control.RelatedDataDefinition.RelatedItemSource = Telerik.Sitefinity.RelatedData.Web.UI.RelatedItemSource.Url;
}
}
Here is a video demonstration of the widget:
You can get the full source code and fork it from GitHub here.
Nikola Zagorchev
Nikola Zagorchev is a Tech Support Engineer at Telerik. He joined the Sitefinity Support team in March 2014.