+1-888-365-2779
Try Now
More in this section
Categories
Bloggers
Blogs RSS feed

Creating one to many relationships in dynamic modules

by Radoslav Georgiev

(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

  1. Download the source code and open it.
  2. Extract the Telerik.Sitefinity.Samples folder from the archive to a location of your choice. 
  3. Open your web project in Visual Studio
  4. Add an existing project to the solution, and locate the Telerik.Sitefinity.Samples from the extracted folder.
  5. Resolve the broken assembly references by pointing them to the bin folder of your web project.
  6. Include a project reference to the Telerik.Sitefinity.Samples project in your web project.
  7. Run your project and follow set up instructions from the bellow video.
Unable to display content. Adobe Flash is required.

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>

15 comments

Leave a comment
  1. Steve Feb 27, 2012
    Will this be forward compatible?

    Like I assume there will be a UI sometime after 5.0 to do this w/out this code...so if we do this now will there be any fixing we need to do once you guys implement it? :)

    I just want to know if we should go ahead with this now, or wait, or go ahead and go back later to fix.

    Thanks Rado\SF!
  2. Radoslav Georgiev Feb 27, 2012
    Hi Steve,

    This will be forward compatible. Even when we add interface for those fields you will be able to keep using the UI you have set the field to use.
  3. Richard Baugh Mar 07, 2012
    I modified the RelateditemsField.js file. What I found was that if you selected the same item, it would add it to the array again rather than checking to see if the item was already in the array before adding. I know that you can prevent this from just not selecting one that you have already selected, but I can't always expect the end user to abide by this.

    _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++) {
                // check to see if the item already exists, if not then add it
                if (this.findItem(data, selectedItems[i]) < 0)
                    data.push(selectedItems[i]);
            }
            //reapply the data context
            ko.mapping.fromJS(data, this._selectedItems);
        }
        this._selectDialog.dialog("close");
    },
     
    findItem: function (array, item) {
        for (var i = 0; i < array.length; i++) {
            if (array[i].Id === item.Id)
                return i;
        }
        return -1;
    }

    As you can see I added a findItem function that will take the current "data" array and check to see if the selected item already exists or not. I had to use the Id property to compare with as the selected item object itself would not compare. Then in the _doneLinkClicked function, I am adding an if statement to check if the item exists.
  4. Daniel Plomp Mar 08, 2012
    Hi what can this error mean?

    There was an error deserializing the object of type Telerik.Sitefinity.Web.Services.ItemContext`1[[Telerik.Sitefinity.DynamicModules.Model.DynamicContent, Telerik.Sitefinity.Model, Version=5.0.2500.0, Culture=neutral, PublicKeyToken=b28c218413bdf563]]. End element 'School' from namespace '' expected. Found element 'item' from namespace ''.

    Not sure where to find it.

    Thanks,
    Daniel
  5. Daniel Plomp Mar 08, 2012
    Hi what can this error mean?

    There was an error deserializing the object of type Telerik.Sitefinity.Web.Services.ItemContext`1[[Telerik.Sitefinity.DynamicModules.Model.DynamicContent, Telerik.Sitefinity.Model, Version=5.0.2500.0, Culture=neutral, PublicKeyToken=b28c218413bdf563]]. End element 'School' from namespace '' expected. Found element 'item' from namespace ''.

    Not sure where to find it.

    Thanks,
    Daniel
  6. eb-1988 Apr 20, 2012
    Daniel, you must select Array of GUID by adding the field. After this all works.
  7. rory Apr 27, 2012
    hey all.  how would i go about getting this selector to populate previously selected items?  

    as is, if you select some items and click done they get saved.  but if you click on select again they are all unselected.  how can i make previously selected items selected on load?

    i have been trying to figure it out in the _selectLinkClicked event but i cant figure out
    A) how to loop through items in this._selectDialog.
    B) select the specific items based on their Id.

    i have spent sooo much time trying to figure out the most basic sitefinity 5 stuff, it hurts.
  8. rory Apr 27, 2012
    hey all.  how would i go about getting this selector to populate previously selected items?  

    as is, if you select some items and click done they get saved.  but if you click on select again they are all unselected.  how can i make previously selected items selected on load?

    i have been trying to figure it out in the _selectLinkClicked event but i cant figure out
    A) how to loop through items in this._selectDialog.
    B) select the specific items based on their Id.

    i have spent sooo much time trying to figure out the most basic sitefinity 5 stuff, it hurts.
  9. rory Apr 27, 2012
    hey all.  how would i go about getting this selector to populate previously selected items?  

    as is, if you select some items and click done they get saved.  but if you click on select again they are all unselected.  how can i make previously selected items selected on load?

    i have been trying to figure it out in the _selectLinkClicked event but i cant figure out
    A) how to loop through items in this._selectDialog.
    B) select the specific items based on their Id.

    i have spent sooo much time trying to figure out the most basic sitefinity 5 stuff, it hurts.
  10. John May 09, 2012
    @rory,

    Hi Rory,

    I had the same problem and this is how I resolved it:

    There are 2 places in the project to update in order for it to work with your content type. First you change the string on line 217 in the file ~/FieldControls/RelatedItemsField.cs to match the type you are trying to load in your sitefinity project. This will allow the field to query and load the content items into the popup to select what you want to relate. The second place is in ~/Resources/Scripts/RelateditemsField.js, line 110. This should have the same string value assigned in the previous location. This allows the page to load in your previously saved items when you go back and view the content item you have created.

    - John
  11. John May 09, 2012
    @rory,

    Hi Rory,

    I had the same problem and this is how I resolved it:

    There are 2 places in the project to update in order for it to work with your content type. First you change the string on line 217 in the file ~/FieldControls/RelatedItemsField.cs to match the type you are trying to load in your sitefinity project. This will allow the field to query and load the content items into the popup to select what you want to relate. The second place is in ~/Resources/Scripts/RelateditemsField.js, line 110. This should have the same string value assigned in the previous location. This allows the page to load in your previously saved items when you go back and view the content item you have created.

    - John
  12. rory May 10, 2012
    thanks John. will try that out.
  13. rory May 10, 2012
    @ John

    Turns out i already have those fields matching.  I set the ItemType in the .cs file and then add that value as a property to the .js file and use that as the itemtype on the ajax call.

    still no joy, and i am using an updated version of the above code from the sitefinity guys themselves.

    thanks anyway.  
  14. Artur Dec 04, 2012
    Where can i download the code? I can't find a link.
  15. Chris Jan 14, 2013
    I second that question. Where's the download link?

    Leave a comment