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 Methodsprotected 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;}#endregionThis 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>
Subscribe to get all the news, info and tutorials you need to build better business apps and sites