Multiple Media selector for Sitefinity Widget designers

November 17, 2014 Digital Experience

In this blog post I will show how you can implement a multiple media selector, using which, you will be able to select multiple images, documents and files or videos at once.

The content selectors in Sitefinity use the services provided, passing different parameters and filtering using them. Most of the non hierarchical selectors are based on the FlatSelector, which inherits from the ItemSelector.They provide out of the box binding, item selection, paging and searching. We have only to ensure the binder of the selector is bound to the service we want and it sets the parameters for item type, sort and filter in the right order the service expects. 

You could be familiar with the above mentioned selectors, since they are used in the automatically generated field controls and designers for selecting of dynamic content. I will show you how you can modify these selectors and build on top of them. This way you can easily bind to any service in Sitefinity and modify the parameters order and values. In this specific post I am going to provide an implementation of an images selector. It can be bound to documents and files or videos service, as well. It provides also a library filtering. This way I can show how you can take advantage of other controls coming from Sitefinity or custom built. In this case, I will be using an abstraction based on top of the FolderSelector customized especially for the scenario I want - selection of libraries and passing only the library id.

 

Configuring the selector 

The selector template is a usual setup of the FlatSelector, we set the service Url to bind, the template of the bound items, as well as, the kendo template of the selected items. The kendo template is used for the observable object - the list of items it is bound to.

<li class="sfFormCtrl">
            <label class="sfTxtLbl" for="selectedTestLabel">ImagesSelector</label>
            <div id="ImagesSelector">
                <div class="left">
                    <sf:FlatSelector ID="ItemSelector" runat="server" ItemType="Telerik.Sitefinity.Libraries.Model.Image"
                        DataKeyNames="Id" ShowSelectedFilter="true" AllowPaging="true" PageSize="3" AllowMultipleSelection="true"
                        AllowSearching="true" ShowProvidersList="false" InclueAllProvidersOption="false" EnablePersistedSelection="true"
                        SearchBoxTitleText="Filter by Title" ShowHeader="true" ServiceUrl="/Sitefinity/Services/Content/ImageService.svc/">
                        <DataMembers>
                            <sf:DataMemberInfo runat="server" Name="Title" IsExtendedSearchField="true" HeaderText='Title'>
                                <div><img alt="Alternate Text" class="tmb" /><span>{{ThumbnailUrl}}</span></div>
                <strong>{{Title}}</strong>
                            </sf:DataMemberInfo>
 
                            <sf:DataMemberInfo runat="server" Name="PublicationDate" HeaderText='Date'>
                <span>{{PublicationDate ? PublicationDate.sitefinityLocaleFormat('dd MMM, yyyy') : ""}}</span>
                            </sf:DataMemberInfo>
                        </DataMembers>
                    </sf:FlatSelector>
                </div>
                <div class="right">                  
                    <sf:MyLibrarySelector ID="est" runat="server" DisplayMode="Write"></sf:MyLibrarySelector>
                </div>
                <asp:Panel runat="server" ID="buttonAreaPanel" class="sfButtonArea sfSelectorBtns">
                    <asp:LinkButton ID="lnkDone" runat="server" OnClientClick="return false;" CssClass="sfLinkBtn sfSave">
                <strong class="sfLinkBtnIn">
                    <asp:Literal runat="server" Text="<%$Resources:Labels, Done %>" />
                </strong>
                    </asp:LinkButton>
                    <asp:Literal runat="server" Text="<%$Resources:Labels, or%>" />
                    <asp:LinkButton ID="lnkCancel" runat="server" CssClass="sfCancel" OnClientClick="return false;">
                <asp:Literal runat="server" Text="<%$Resources:Labels, Cancel %>" />
                    </asp:LinkButton>
                </asp:Panel>
            </div>
            <ul id="selectedItemsList" runat="server" data-template="ul-template-ImagesSelector" data-bind="source: items" class="sfCategoriesList"></ul>
            <script id="ul-template-ImagesSelector" type="text/x-kendo-template">
    <li>
    <div data-id="#: Id#"><img src="#: ThumbnailUrl#" alt="#: Title#" class="tmb" /></div>
        <span data-bind="text: Title, attr: {data-id: Id}"> </span>
  
        <a class="remove sfRemoveBtn">Remove</a>
    </li>
            </script>
            <asp:HyperLink ID="selectButton" runat="server" NavigateUrl="javascript:void(0);" CssClass="sfLinkBtn sfChange">
    <strong class="sfLinkBtnIn">Add items...</strong>
            </asp:HyperLink>
        </li>

The server side code finds the controls we will need to work with client-side and pass them to the ScriptDescriptor:

/// <summary>
        /// Gets a collection of script descriptors that represent ECMAScript (JavaScript) client components.
        /// </summary>
        public override System.Collections.Generic.IEnumerable<System.Web.UI.ScriptDescriptor> GetScriptDescriptors()
        {
            var scriptDescriptors = new List<ScriptDescriptor>(base.GetScriptDescriptors());
            var descriptor = (ScriptControlDescriptor)scriptDescriptors.Last();
 
            descriptor.AddElementProperty("message", this.Message.ClientID);
            descriptor.AddElementProperty("selectButton", this.SelectButton.ClientID);
            descriptor.AddComponentProperty("ItemSelector", this.ItemSelector.ClientID);
            descriptor.AddElementProperty("lnkDone", this.DoneButton.ClientID);
            descriptor.AddElementProperty("lnkCancel", this.CancelButton.ClientID);
            descriptor.AddElementProperty("selectedItemsList", this.SelectedItemsList.ClientID);
 
            descriptor.AddComponentProperty("myLibrarySelector", this.LibrarySelector.ClientID);
 
            return scriptDescriptors;
        }

Now we can operate with all the controls we need in the JavaScript of the Designer.

In order to configure the selector to bind correctly to all content services, we have to configure the order of the parameters. In order to do this, we have to replace the default binding of the selector, since there is the Url params setting:

_bindSelector: function () {
        var urlParams = this._binder.get_urlParams();
        urlParams['itemType'] = this._itemType;
        if (this._itemSurrogateType != null)
            urlParams['itemSurrogateType'] = this._itemSurrogateType;
        urlParams['allProviders'] = (this._providerName == "" || this._providerName == null);
        if (this.get_combinedFilter())
            urlParams['filter'] = this.get_combinedFilter();
        urlParams.filter = "";
        this._binder.set_provider(this._providerName);
        this._binder.DataBind();
    },

Set the filter parameter as last. Then, after the binding has begun, we need to select the already chosen items:

this._binderDataBindingDelegate = Function.createDelegate(this, this._binderDataBindingHandler);
        this._ItemSelector.add_binderDataBinding(this._binderDataBindingDelegate);
 
_binderDataBindingHandler: function (sender, args) {
        var selectedItems = this._selectedItems.items.toJSON();
        var itemSelector = this.get_ItemSelector();
        if (selectedItems) {
            var items = args.get_dataItem().Items;
            for (var i = 0; i < selectedItems.length; i++) {
                var selectedItem = selectedItems[i];
                itemSelector.selectItem(selectedItem.Id, selectedItem);
            }
        }
    },

We need to attach to the library selector events used, as well, so when we want to filter the items by the library, we just change the base service and add the parent parameter and the album Id:

        this.get_myLibrarySelector()._dataBind('albums');
        this.get_myLibrarySelector()._addSelectionChanged(this._librarySelectionChanged);
        this.get_myLibrarySelector()._addAllLibrariesSelected(this._resetSelectors);
 
_librarySelectionChanged: function (sender, args) {
        if (args && args[0] && args[0].Id) {
            _selfDesigner.get_ItemSelector()._binder._serviceBaseUrl = _selfDesigner._serviceUrl + "parent/" + args[0].Id + "/";
            _selfDesigner.get_ItemSelector().dataBind();
        }
        else {
            _selfDesigner._resetSelectors();
        }
    },
 
    _resetSelectors: function () {
        _selfDesigner.get_ItemSelector().get_binder()._serviceBaseUrl = _selfDesigner._serviceUrl;
        _selfDesigner.get_myLibrarySelector()._clearSelection();
        _selfDesigner.get_ItemSelector().dataBind();
    },

We need to rebind when chaning the parent folder, so the selector could re-build its paging. Note that this way, the selected items will be shown only from the current library.

Saving and Showing the selected items

We save all the items initially selected and newly added in a kendo observable. This way the selected items in the designer are automatically refreshed.

The items are loaded using a call to the service and passing a filter using the items Ids:

var value = controlData.SelectedImagesIds;
       if (value != null && value != "") {
           var dataItems = JSON.parse(value);
           var filterExpression = "";
           for (var i = 0; i < dataItems.length; i++) {
               if (i > 0) {
                   filterExpression = filterExpression + ' OR ';
               }
               filterExpression = filterExpression + 'Id == ' + dataItems[i].toString();
           }
           var data = {
               "filter": filterExpression,
 
               "itemType": this._itemsType,
 
               "provider": this._providerName,
           };
 
           var self = this;
           $.ajax({
               url: this._serviceUrl,
               type: "GET",
               dataType: "json",
               data: data,
               headers: { "SF_UI_CULTURE": "en" },
               contentType: "application/json; charset=utf-8",
               /*on success add them to the kendo observable array*/
               success: function (result) {
                   self._resizeControlDesigner();
                   self._selectedItems.items.splice(0, self._selectedItems.items.length);
                   for (var i = 0; i < result.Items.length; i++) {
                       self._selectedItems.items.push(result.Items[i]);
                   }
               }
           });
       }

The selected items are updated upon selection done in the selector:

if (this._lnkDone) {
            this._DoneSelectingDelegate = Function.createDelegate(this, this._DoneSelecting);
            $addHandler(this._lnkDone, "click", this._DoneSelectingDelegate);
        }
 
_DoneSelecting: function (sender, args) {
        this._selectedItems.items.splice(0, this._selectedItems.items.length);
 
        var selectedItems = this.get_SelectedItems();
        if (selectedItems != null && selectedItems.length > 0) {
            var data = selectedItems;
            for (var i = 0; i < data.length; i++) {
                this._selectedItems.items.push(data[i]);
            }
        }
        this._selectDialog.dialog("close");
        jQuery("#designerLayoutRoot").show();
        dialogBase.resizeToContent();
    },

You can download the full source from GitHub.

Here is a short video of the selector and designer:

Nikola Zagorchev

Nikola Zagorchev is a Tech Support Engineer at Telerik. He joined the Sitefinity Support team in March 2014.