The content you're reading is getting on in years
This post is on the older side and its content may be out of date.
Be sure to visit our blogs homepage for our latest news, updates and information.
(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.
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.
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
>
...
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
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);
},
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
>
View all posts from The Progress Team on the Progress blog. Connect with us about all things application development and deployment, data integration and digital business.
Let our experts teach you how to use Sitefinity's best-in-class features to deliver compelling digital experiences.
Learn MoreSubscribe to get all the news, info and tutorials you need to build better business apps and sites