(With Sitefinity version 5.1 and above there is a simple way to relate dynamic modules and here is a blog post that summarizes the process)
With the Sitefinity 5.0 release we are introducing two new field types for the Module Builder - Guid and Array of Guids. This blog post is going to focus on the latter and sample the process of creating a field control which allows you to associate one item from a dynamic module with other items (from the same module or other dynamic modules). This field control relies on KnockoutJS and the KnockoutJS mapping plugin to bind and manage the related items in the write mode of our field control.
Creating the field control
Instead of creating a field control from scratch we are going to use the sample field control outlined here: Extend the Image Selector for Content Items with Filtering by Album, and remove the unnecessary logic for our case. We need to implement the field control class, template, its definition and configuration elements and script component.
Field control template
The field control template is going to include content selector which will pop up in a window to select related items and a unordered list which will display selected items and let us manipulate the collection. KnockoutJS is going to automatically handle binding of the list and removing items:
...
<
sf:ConditionalTemplate
Left
=
"DisplayMode"
Operator
=
"Equal"
Right
=
"Write"
runat
=
"server"
>
<
sf:SitefinityLabel
ID
=
"titleLabel_write"
runat
=
"server"
CssClass
=
"sfTxtLbl"
/>
<
asp:LinkButton
ID
=
"expandButton_write"
runat
=
"server"
OnClientClick
=
"return false;"
CssClass
=
"sfOptionalExpander"
/>
<
asp:Panel
ID
=
"expandableTarget_write"
runat
=
"server"
CssClass
=
"sfFieldWrp"
>
<
div
div
id
=
"selectorTag"
style
=
"display: none;"
class
=
"sfDesignerSelector sfFlatDialogSelector"
>
<
designers:ContentSelector
ID
=
"selector"
runat
=
"server"
TitleText
=
"Choose items"
BindOnLoad
=
"false"
WorkMode
=
"List"
SearchBoxInnerText
=
""
SearchBoxTitleText="<%$Resources:Labels, NarrowByTypingTitleOrAuthorOrDate %>"
ListModeClientTemplate="<
strong
class
=
'sfItemTitle'
>{{Title}}</
strong
><
span
class
=
'sfDate'
>{{PublicationDate ? PublicationDate.sitefinityLocaleFormat('dd MMM yyyy') : ""}} by {{Author}}</
span
>">
</
designers:ContentSelector
>
</
div
>
<
ul
id
=
"selectedItemsList"
data-bind
=
"foreach: items"
>
<
li
>
<
span
data-bind
=
"text: Title"
> </
span
>
<
a
href
=
"#"
class
=
"remove"
>Remove</
a
>
</
li
>
</
ul
>
<
asp:TextBox
ID
=
"textBox_write"
runat
=
"server"
Style
=
"display:none"
CssClass
=
"sfTxt"
/>
<
asp:HyperLink
ID
=
"selectLink"
runat
=
"server"
NavigateUrl
=
"javascript:void(0);"
CssClass
=
"sfLinkBtn sfChange"
>
<
strong
class
=
"sfLinkBtnIn"
>Select...</
strong
>
</
asp:HyperLink
>
<
sf:SitefinityLabel
id
=
"descriptionLabel_write"
runat
=
"server"
WrapperTagName
=
"div"
HideIfNoText
=
"true"
CssClass
=
"sfDescription"
/>
<
sf:SitefinityLabel
id
=
"exampleLabel_write"
runat
=
"server"
WrapperTagName
=
"div"
HideIfNoText
=
"true"
CssClass
=
"sfExample"
/>
</
asp:Panel
>
</
sf:ConditionalTemplate
>
...
Field control class
The field control class needs to include a reference to the script component and the KnockoutJS libraries. It will also pass as a script script descriptor the items selector, the button which opens it and the path to the dynamic content data service. Excerpt from the class bellow:
#region Overridden Methods
protected
override
void
InitializeControls(GenericContainer container)
{
this
.ConstructControl();
this
.ItemsSelector.ServiceUrl =
string
.Concat(
"~/Sitefinity/Services/DynamicModules/Data.svc/"
);
//set item type for the items we want to select from
this
.ItemsSelector.ItemType =
"Telerik.Sitefinity.DynamicTypes.Model.Releasenotes.ReleaseNote"
;
this
.ItemsSelector.AllowMultipleSelection =
true
;
}
#endregion
#region IScriptControl Members
/// <summary>
/// Gets the script references.
/// </summary>
/// <returns></returns>
public
override
IEnumerable<ScriptReference> GetScriptReferences()
{
var baseReferences =
new
List<ScriptReference>(
base
.GetScriptReferences());
var componentRef =
new
ScriptReference(relatedItemsFieldScript,
this
.GetType().Assembly.FullName);
//add references to knockoutJS and knockoutJS mapping plugin
var knockOutRef =
new
ScriptReference(knockOutScript,
this
.GetType().Assembly.FullName);
var knockOutMappingRef =
new
ScriptReference(knockOutMappingScript,
this
.GetType().Assembly.FullName);
var jqueryUIScript =
new
ScriptReference(
"Telerik.Sitefinity.Resources.Scripts.jquery-ui-1.8.8.custom.min.js"
,
"Telerik.Sitefinity.Resources"
);
baseReferences.Add(componentRef);
baseReferences.Add(knockOutRef);
baseReferences.Add(knockOutMappingRef);
baseReferences.Add(jqueryUIScript);
return
baseReferences;
}
/// <summary>
/// Gets the script descriptors.
/// </summary>
/// <returns></returns>
public
override
IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
var lastDescriptor = (ScriptControlDescriptor)
base
.GetScriptDescriptors().Last();
if
(
this
.DisplayMode == FieldDisplayMode.Write)
{
lastDescriptor.AddElementProperty(
"selectLink"
,
this
.SelectLink.ClientID);
lastDescriptor.AddComponentProperty(
"itemsSelector"
,
this
.ItemsSelector.ClientID);
lastDescriptor.AddProperty(
"dynamicModulesDataServicePath"
, RouteHelper.ResolveUrl(dynamicModulesDataServicePath, UrlResolveOptions.Rooted));
}
if
(
this
.DisplayMode == FieldDisplayMode.Read)
{
lastDescriptor.AddElementProperty(
"imageControl"
,
this
.ImageControl.ClientID);
}
yield
return
lastDescriptor;
}
#endregion
Field control client component
This is where all the actual logic is placed. First we need to implement the get_value and set_value methods required for all field controls. Those methods are called by Sitefinity to populate the field control and get the values for persisting data set in the field control.
// Gets the value of the field control.
get_value:
function
() {
//on publish if we have items in the knockout data context
//we get their ids in a aray of Guids so that they can be persisted
var
selectedKeysArray =
new
Array();
var
data = ko.mapping.toJS(
this
._selectedItems);
for
(
var
i = 0; i < data.length; i++) {
selectedKeysArray.push(data[i].Id);
}
if
(selectedKeysArray.length > 0)
return
selectedKeysArray;
else
return
null
;
},
// Sets the value of the text field control depending on DisplayMode.
set_value:
function
(value) {
if
(
this
._hideIfValue !=
null
&&
this
._hideIfValue == value) {
}
else
{
//if there are related items get them through the dynamic modules' data service
if
(value !=
null
&& value !=
""
) {
var
filterExpression =
""
;
for
(
var
i = 0; i < value.length; i++) {
if
(i == 0)
filterExpression = filterExpression +
'Id == '
+ value[i].toString();
else
filterExpression = filterExpression +
' OR Id == '
+ value[i].toString();
}
var
data = {
"itemType"
:
"Telerik.Sitefinity.DynamicTypes.Model.Releasenotes.ReleaseNote"
,
"filter"
: filterExpression
};
$.ajax({
url:
this
.get_dynamicModulesDataServicePath(),
type:
"GET"
,
dataType:
"json"
,
data: data,
contentType:
"application/json; charset=utf-8"
,
//on success add them to the knockout data context
success:
this
._getSelectedItemsSuccesssDelegate
});
}
}
this
.raisePropertyChanged(
"value"
);
this
._valueChangedHandler();
},
_getSelectedItemsSuccesss:
function
(result) {
//push existing related items in the knockout data context
ko.mapping.fromJS(result.Items,
this
._selectedItems);
},
We also need one event handler that will be called when the dialog for selecting items closes to push them in the Knockout data context and one event handler which is raised when the remove link for a respective item is clicked.
_doneLinkClicked:
function
(keys) {
if
(keys !=
null
) {
//push newly selected items in the list of selected items
var
data = ko.mapping.toJS(
this
._selectedItems);
var
selectedItems =
this
.get_itemsSelector().getSelectedItems();
for
(
var
i = 0; i < selectedItems.length; i++) {
data.push(selectedItems[i]);
}
//reapply the data context
ko.mapping.fromJS(data,
this
._selectedItems);
}
this
._selectDialog.dialog(
"close"
);
},
_removeItem:
function
(value) {
var
context = ko.contextFor(value.currentTarget);
this
._selectedItems.remove(context.$data);
},
Installation Instructions
- Download the source code and open it.
- Extract the Telerik.Sitefinity.Samples folder from the archive to a location of your choice.
- Open your web project in Visual Studio
- Add an existing project to the solution, and locate the Telerik.Sitefinity.Samples from the extracted folder.
- Resolve the broken assembly references by pointing them to the bin folder of your web project.
- Include a project reference to the Telerik.Sitefinity.Samples project in your web project.
- Run your project and follow set up instructions from the bellow video.
Note: The field control is using the Virtual Path provider to resolve its template. You need to register a VP as bellow:
<
virtualPaths
>
...
<
add
resourceLocation
=
"Telerik.Sitefinity.Samples"
resolverName
=
"EmbeddedResourceResolver"
virtualPath
=
"~/SfSamples/*"
/>
</
virtualPaths
>