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.
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.
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.
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 is a Tech Support Engineer at Telerik. He joined the Sitefinity Support team in March 2014.
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
Progress collects the Personal Information set out in our Privacy Policy and the Supplemental Privacy notice for residents of California and other US States and uses it for the purposes stated in that policy.
You can also ask us not to share your Personal Information to third parties here: Do Not Sell or Share My Info
We see that you have already chosen to receive marketing materials from us. If you wish to change this at any time you may do so by clicking here.
Thank you for your continued interest in Progress. Based on either your previous activity on our websites or our ongoing relationship, we will keep you updated on our products, solutions, services, company news and events. If you decide that you want to be removed from our mailing lists at any time, you can change your contact preferences by clicking here.