This document provides step by step instructions on how
to build Brochure Builder Sitecore SPEAK application. The controls that were
used to create this application include a custom SearchDataSource,
SelectedItemsList that allows users see selected for brochure items and
change their order in the list, another custom control that is responsible
for calling an MVC controller to trigger the brochure build and execute
Sitecore rules upon completion. You will see how custom controls with custom
properties can be created and how those properties can be accessed in the
JavaScript code.
Those of you who attended my presentation
“Creating Dynamic Brochures with Sitecore PXM Module” at
Sitecore Symposium 2014 in Las Vegas or Barcelona have seen a demo of this application. For those of you who have not seen it, here is a screenshot and description
of the final product.
|
Brochure Builder Application |
As you can see there are a few controls on the application page. All of them except “Selected for Brochure” one on the left are Sitecore out-of-the-box ones. However, because Weil.com solution uses Solr as a search engine, I had to implement a custom data source for the main
ListControl that list all available for selection items. I also had to make sure that we only display items that are allowed to be rendered in a PDF brochure since this is a Brochure Builder application.
Let’s try to build this application from scratch. If you are new to SPEAK, I would suggest reading
Martina Welander’s blogs at
http://mhwelander.net/category/speak/ first. It will give you basics of SPEAK as well as great detail on the overall structure and hidden jams of it.
Now you are familiar with how the application should look
like. It is time to start building it!
First of all I need to create an application page. To do
that I’m going to open Sitecore Rocks in Visual Studio, connect to
core
database in a Sitecore instance and add a new item named
ContentSelection under
“
/sitecore/client/[WeilApp]/Brochure Builder”. You can choose a
different location for your application if you would like.
[WeilApp] can
be replaced with the name of the folder you decide to keep all your custom
applications in.
I chose to use a
Dialog ListPage for the application
that I created for the symposium, so I’ll do the same here.
When the
ListPage is created from a branch, there are
a few renderings already assigned to the layout settings of the newly created page.
PageSettings item is also created automatically. To see what those
layout settings are, you can hit [Ctrl+U] or right click on your list page and
go to
Tasks > Design Layout like you see on the screen shot below.
|
How to view the layout settings for an application page. |
When the Layout settings window opens you should see
something like this:
|
Initial Renderings on a ListPage. |
There are quite a few renderings are already assigned to the
page. Now I can start assigning new renderings that I need for the Brochure
Builder. But before I do that, let’s take a look at how initial page looks
like.
|
Initial view of Brochure Builder application. |
Not much there, right? If you have done some SPEAK
development in the past, you have seen this page many times before.
If application page is expected to take parameters, and in a
case of a
Dialog that is often the case you can create a class in your
solution that would be responsible for processing passed in parameters. You can
also create a custom JavaScript file to extend default page functionality.
The code in a back end class for a dialog page would look
something like this:
public class BuildBrochureDialog : PageCodeBase
{
public Rendering DataSource { get; set; }
public override void Initialize()
{
this.ReadQueryParamsAndUpdatePlaceholders();
}
private void ReadQueryParamsAndUpdatePlaceholders()
{
string queryString1 = WebUtil.GetQueryString("ro");
string queryString2 = WebUtil.GetQueryString("hdl");
if (!string.IsNullOrEmpty(queryString1) && queryString1 != "{0}")
this.DataSource.Parameters["RootItemId"] = !ItemUri.IsItemUri(queryString1) ? (ClientHost.Items.GetItem(queryString1) ?? Clien-tHost.Databases.Database.GetRootItem()).ID.ToString() : (Clien-tHost.Items.GetItem(ItemUri.Parse(queryString1).GetPathOrId()) ?? Clien-tHost.Databases.Database.GetRootItem()).ID.ToString();
// put your code here
}
}
If query string parameters can be passed to your application,
you can process them in this manner and assign the values to controls you need.
When the class is created you can update the
PageCode rendering
PageCodeTypeName property to use it.
If you need to extend JavaScript functionality of the
PageCode
rendering, you can create your own
.js file that would have the code similar to
the one below.
define(["sitecore", "jquery"], function (Sitecore, jQuery) {
var BrochureBuilderPageCode = Sitecore.Definitions.App.extend({
initialized: function () {
},
doSomething: function () {
//custom logic goes here
}
});
return BrochureBuilderPageCode;
});
In the
PageCode rendering properties window you will
need to update the
PageCodeScriptName property
to point to the
.js file.
The first rendering that I am going to add is the
ScrollBar.
It will enable the
Scrollbar behavior on the
ListControl I’m
going to add a little later.
Since I am building a “Brochure Builder” application and not
just “List”, I need to make the page title say so. I can do it in two ways, and
you’ll see me do the same
Text change for other renderings using both approaches,
but for this control I am going to choose the data source approach and to add a
Text item under
PageSettings with the
HeaderTitle name.
In newly created
HeaderTitle item I am going to enter “Brochure Builder” in the
Text field.
The next step is to point the
Header Title rendering
to the new
HeaderTitle
item as a
Data Source.
I’ll also need to make sure that the
Text field on the rendering is empty;
otherwise it will override the value from the
Data Source.
As a result the title update, the header now says “Brochure
Builder”. Very nice!
There are two buttons in the header section of the dialog –
OK
and
Cancel. “
Cancel” fits right in, however “OK” doesn’t
represent the action that it is supposed to trigger – building of a brochure.
So, let’s rename this button to “Build Brochure” and make it start brochure
building process.
First I’m going to add a new
Text item under
PageSettings
and call it
BuildButton. In the item
Text
field I’m going to enter “Brochure Builder”.The next step is renaming the
OKbutton
to be
BuildButton and updating its properties.
I need to point the
Data Source to the
BuildButton item under the
PageSettings and specify
the
click event. I’m going to enter
app.BrochureBuilderObserver.build()
JavaScript function in the
click field. I’m going to add the BrochureBuilderObserver control later in
Chapter
2.16.5. After all updates are made, the rendering properties windows
should have the following settings:
Notice that the
Text field is empty, Id is changed to
BuildButton and
Click event now has
“javascript:app.BrochureBuilderObserver.build()”.
To inform user about completion of brochure build, I added a
Text rendering called
BuildCompleteText.
It doesn’t have a data source. Message text is stored in
Text field directly in
rendering properties.
IsVisible
property is set to
False to hide the message when page initially loads.
The placeholder for this rendering is set to be
DialogContent.Main to
display the message in the main section of application window. Later I’m going to add two Rule renderings called
BuildIsDoneRule
and
BuildFailedRule
that will update the text property value of
the
BuildCompleteText depending on whether
the build succeeded or failed.
When brochure is built and ready for viewing, I need to display a button that
would allow user to open up created PDF file. For that I am going to add a DownloadButton
to the list of renderings.
In properties window I will add the following settings:
- ButtonType is set to Default
- IsVisible is False to hide the button when page initially loads
- Text for the button is “Open
Brochure”
- Click event – javascript:app.BrochureBuilderObserver.openBrochure(). This is a JavaScript event that is implemented in the BrochureBuilderObeserver rendering that I’m going to add later.
When build is done I would like to offer a user an option to
build a new brochure. For that I’m going to add a new
Button rendering called
BuildAnotherButton
and assign the following properties to it.
In the
Text field I’m
going to add “Build New Brochure” to be displayed on the button. I am also
going to choose “Primary” in the
ButtonType field
to make it red. The button should not be visible when the page initially loads,
so I need to set
IsVisible to
False. And the last, but not least, the property value
that will make application initial page to be loaded when the button is clicked.
The click property value needs to be set to:
javascript:window.location=’/sitecore/client/WeilApps/Brochure
Builder/Content Selection’
To create a layout structure for the search controls, I’m
going to add a
SearchPanel rendering called
Search to the list of renderings in the
DialogContent.Main
placeholder.
Weil.com solution uses
Solr as a search engine, and I
need to make sure I use the same search engine as the rest of the solution
does. To accommodate that I am going to create a custom SearchDataSource rendering that would go through
the same pipes as the web site pages do to retrieve the content based on the search
criteria.
First
I am going to copy the
SearchDataSource rendering
from
/sitecore/client/Speak/Layouts/Renderings/Data/SearchDataSource
into my application and rename it to be
CustomSearchDataSource.
If you open
CustomSearchDataSource item,
you’ll see that it has a Path field that
point to the rendering
.cshtml file. Since I
copied this rendering from
SearchDataSource one,
it still points to the
SearchDataSource.cshtml
file. I need to create my own rendering view to make it truly custom. So, I am
going to open my solution explorer and add a new view file under
/sitecore/shell/client/Speak/Layouts/Renderings/Data/CustomSearchDataSources
folder and call it
CustomSearchDataSource.cshtml.
The next step is to copy the code from
SearchDataSouse.cshtml into my view and change the
control name to match mine.
@using Sitecore.Mvc
@using Weil.SC.Client.Speak.UI.Controls.CustomSearchDataSources
@model Sitecore.Mvc.Presentation.RenderingModel
@Html.Sitecore().Controls().CustomSearchDataSource(this.Model.Rendering)
As you might have noticed I also added a reference to
Weil.SC.Client.Speak.UI.Controls.CustomSearchDataSources.
Well, to make
@Html.Sitecore().Controls()CustomSearchDataSource(this.Model.Rendering)
line work I had to add two classes:
- CustomSearchDataSource.cs –
responsible for generating html output for my control;
- ControlExtensions.cs – contains
html helper methods for my control.
Below is the code for the
CustomSearchDataSourc.cs
class.
public class CustomSearchDataSource : ItemDataSourceBase, IPageble
{
public string DatabaseName { get; set; }
public string LanguageName { get; set; }
public ID FacetsRootItemId { get; set; }
public string Formatting { get; set; }
public int PageIndex { get; set; }
public string PageIndexBinding { get; set; }
public int PageSize { get; set; }
public string PageSizeBinding { get; set; }
public ID RootItemId { get; set; }
public string SearchConfigItemId { get; set; }
public string Sorting { get; set; }
public string Text { get; set; }
protected string RootItemIdBinding { get; set; }
protected string SelectedFacets { get; set; }
protected string SelectedFacetsBinding { get; set; }
protected string TextBinding { get; set; }
public CustomSearchDataSource()
{
this.Requires.Script("controls", "customsearchdatasource.js");
}
public CustomSearchDataSource(RenderingParametersResolver parametersResolver): base(parametersResolver) { Assert.ArgumentNotNull((object)parametersResolver, "parametersResolver");
this.Requires.Script("controls", "customsearchdatasource.js");
this.Text = parametersResolver.GetString("Text", "text");
this.FacetsRootItemId = parametersResolver.GetId("FacetsRootItemId", (ID)null);
this.DatabaseName = parametersResolver.GetString("Database", "database");
this.LanguageName = parametersResolver.GetString("Language", "language");
this.RootItemId = parametersResolver.GetId("RootItemId", "rootItemId", (ID)null);
this.SearchConfigItemId = parametersResolver.GetString("SearchConfigItemId");
this.SelectedFacets = parametersResolver.GetString("SelectedFacets", "selectedFacets");
this.Formatting = parametersResolver.GetString("Formatting", "formatting");
this.Sorting = parametersResolver.GetString("Sorting", "sorting");
this.ResolvePagingParameters(parametersResolver, (IPageble)this);
this.TextBinding = parametersResolver.GetString("TextBinding");
this.RootItemIdBinding = parametersResolver.GetString("RootItemIdBinding");
this.SelectedFacetsBinding = parametersResolver.GetString("SelectedFacetsBinding");
}
protected override void PreRender()
{
base.PreRender();
if (!string.IsNullOrEmpty(this.Text))
this.SetAttribute("data-sc-text", this.Text);
if (!ID.IsNullOrEmpty(this.FacetsRootItemId))
this.SetAttribute("data-sc-facets-root-id", this.FacetsRootItemId.ToString());
if (!ID.IsNullOrEmpty(this.RootItemId))
this.SetAttribute("data-sc-root-id", this.RootItemId.ToString());
if (!string.IsNullOrEmpty(this.LanguageName))
{
string strA = this.LanguageName;
if (string.Compare(strA, "$context_language", StringComparison.InvariantCultureIgnoreCase) == 0)
strA = Context.Language.Name;
this.SetAttribute("data-sc-language", strA);
}
if (!string.IsNullOrEmpty(this.DatabaseName))
{
string strA = this.DatabaseName;
if (string.Compare(strA, "$context_database", StringComparison.InvariantCultureIgnoreCase) == 0)
strA = Context.Database.Name;
else if (string.Compare(strA, "$context_contentdatabase", StringComparison.InvariantCultureIgnoreCase) == 0)
strA = Context.ContentDatabase.Name;
this.SetAttribute("data-sc-database", strA);
}
if (!string.IsNullOrEmpty(this.SearchConfigItemId))
this.SetAttribute("data-sc-searchconfig", this.SearchConfigItemId);
if (!string.IsNullOrEmpty(this.Formatting))
this.SetAttribute("data-sc-formatting", this.Formatting);
if (!string.IsNullOrEmpty(this.Sorting))
this.SetAttribute("data-sc-sorting", this.Sorting);
if (this.PageSize > 0)
this.SetAttribute("data-sc-pagesize", this.PageSize.ToString());
if (this.PageIndex > 0)
this.SetAttribute("data-sc-pageindex", this.PageIndex.ToString());
this.SetAttribute(HtmlTextWriterAttribute.Type, "text/x-sitecore-customsearchdatasource");
if (!string.IsNullOrEmpty(this.TextBinding))
this.AddBinding("text", this.TextBinding);
if (!string.IsNullOrEmpty(this.RootItemIdBinding))
this.AddBinding("rootItemId", this.RootItemIdBinding);
if (!string.IsNullOrEmpty(this.SelectedFacetsBinding))
this.AddBinding("selectedFacets", this.SelectedFacetsBinding);
if (!string.IsNullOrEmpty(this.PageSizeBinding))
this.AddBinding("pageSize", this.PageSizeBinding);
if (string.IsNullOrEmpty(this.PageIndexBinding))
return;
this.AddBinding("pageIndex", this.PageIndexBinding);
}
protected override void Render(HtmlTextWriter output)
{
this.AddAttributes(output);
output.RenderBeginTag(HtmlTextWriterTag.Script);
output.RenderEndTag();
}
}
And here is the code for the extension file:
public static class ControlsExtension
{
public static HtmlString CustomSearchDataSource(this Sitecore.Mvc.Controls controls, Sitecore.Mvc.Presentation.Rendering rendering)
{
Assert.ArgumentNotNull((object)controls, "controls");
Assert.ArgumentNotNull((object)rendering, "rendering");
return new HtmlString(new CustomSearchDataSource(controls.GetParametersResolver(rendering)).Render());
}
public static HtmlString CustomSearchDataSource(this Sitecore.Mvc.Controls controls, string controlId, object parameters = null)
{
Assert.ArgumentNotNull((object)controls, "controls");
Assert.ArgumentNotNull((object)controlId, "controlId");
CustomSearchDataSource searchDataSource = new CustomSearchDataSource((RenderingParametersResolver)controls.GetParametersResolver(parameters));
searchDataSource.ControlId = controlId;
return new HtmlString(searchDataSource.Render());
}
}
Now I can build the solution and add new view path to my
CustomSearchDataSource item. The value for that field is:
/sitecore/shell/client/Speak/Layouts/Renderings/Data/CustomSearchDataSources/CustomSearchDataSource.cshtml.
But wait; didn't I forget one more piece? I did. I forgot to add the JavaScript file that does all the magic, the
customsearchdatasource.js. My
.cs file has the code that adds the reference to it in the control.
public CustomSearchDataSource()
{
this.Requires.Script("controls", "customsearchdatasource.js");
}
So, I need to go back to my solution and add a .js file in the same level where my view lives,
under the /sitecore/shell/client/Speak/Layouts/Renderings/Data/CustomSearchDataSources
and name it CustomSearchDataSource.js.
My control should be working almost the same as the
out-of-the-box
SearchDataSource, but it needs
to retrieve data from Solr. So, I’m going to copy the JavaScript code from
SearchDataSource and make a few changes to make it
work.
Here is what the final code
looks like:
define(["sitecore"], function (Sitecore) {
"use strict";
var model = Sitecore.Definitions.Models.ComponentModel.extend(
{
initialize: function (attributes) {
this._super();
this.set("text", "");
this.set("searchConfig", null);
this.set("rootItemId", null);
this.set("pageSize", 0);
this.set("pageIndex", 1);
this.set("totalItemsCount", 0);
this.set("items", null);
this.set("selectedFacets", []);
this.set("facets", []);
this.set("facetsRootItemId", null);
this.set("formatting", "");
this.set("sorting", "");
this.set("language", "");
this.set("database", "");
this.set("pagingMode", "appending");
this.set("isBusy", false);
this.set("hasItems", false);
this.set("hasNoItems", true);
this.set("hasMoreItems", false);
this.set("showHiddenItems", false);
this.on("change:searchType change:text change:pageSize change:pageIndex change:selectedFacets change:rootItemId change:searchConfig change:sorting change:showHiddenItems", this.refresh, this);
this.isReady = false;
this.pendingRequests = 0;
this.lastPage = 1;
},
refresh: function () {
this.set("pageIndex", 1);
this.lastPage = 1;
this.getItems();
},
next: function () {
this.lastPage++;
this.getItems();
},
buildBrochure: function () {
if (!this.isReady) {
return;
}
},
getItems: function () {
if (!this.isReady) {
return;
}
var search = this.get("text"),
options = this.getOptions(),
url,
selectedFacets;
if (!search && !options.root && !options.searchConfig) {
return;
}
url = "/api/sitecore/SearchApi/Get";
selectedFacets = this.get("selectedFacets");
var selectedFacetsParam;
if (selectedFacets != null && selectedFacets.length > 0) {
selectedFacetsParam = this.getFacets(selectedFacets);
}
var database = this.get("database");
this.pendingRequests++;
this.set("isBusy", true);
_sc.debug("CustomSearchDataSource request: '", url, "', options:", options);
var data = $.ajax({
url: selectedFacetsParam ? url + "?" + selectedFacetsParam : url,
data: { searchstring: this.get("text"), pagenum: options.pageIndex, pagesize: options.pageSize, facetsRootItemId: options.facetsRootItemId, root: options.root },
context: this,
success: function (data) {
this.completed(data.result.items, data.result.totalCount, data.result);
}
});
},
getFacets: function (selectedFacets) {
var result = "";
var facets = {};
_.each(selectedFacets, function (facet) {
if (!facets[facet.name]) {
facets[facet.name] = [];
}
facets[facet.name].push(facet.value);
}, this);
_.each(_.keys(facets), function (name) {
var s = "";
_.each(facets[name], function (i) {
s += (s != "" ? "," : "") + i;
}, this);
result += (result != "" ? "&" : "") + name + "=" + s;
}, this);
return result;
},
getOptions: function () {
var options = {}, fields;
var pageSize = this.get("pageSize");
if (pageSize) {
options.pageSize = pageSize;
if (this.get("pagingMode") == "appending") {
options.pageIndex = this.lastPage;
}
else {
options.pageIndex = this.get("pageIndex");
}
}
fields = this.get("fields");
if (fields && fields.length > 0) {
options.fields = fields;
}
else {
options.payLoad = "full";
}
options.root = this.get("rootItemId");
options.language = this.get("language");
options.facetsRootItemId = this.get("facetsRootItemId");
options.searchConfig = this.get("searchConfig");
if (this.get("formatting") != "") {
options.formatting = this.get("formatting");
}
if (this.get("sorting") != "") {
options.sorting = this.get("sorting");
}
if (this.get("showHiddenItems")) {
options.showHiddenItems = true;
}
return options;
},
completed: function (items, totalCount, result) {
_sc.debug("CustomSearchDataSource received: ", result);
// logic for parsing dates when formatting==$send_localized_dates
if (this.get("formatting") == "$send_localized_dates") {
_.each(items, function (item) {
var formatedFields = [];
_.each(item.$fields, function (field) {
var fieldType = field.type ? field.type.toLowerCase() : '';
if (fieldType === "datetime" || fieldType === "date") {
formatedFields[field.fieldName] = {
type: field.type,
formattedValue: field.formattedValue,
longDateValue: field.longDateValue,
shortDateValue: field.shortDateValue
};
}
});
//extend item with formated fields
item.$formatedFields = formatedFields;
});
}
if (this.get("pagingMode") == "appending" && this.lastPage > 1) {
items = this.get("items").concat(items);
this.set("items", items, { force: true });
}
else {
this.set("items", items, { force: true });
this.set("facets", result.facets ? result.facets : []);
}
this.set("totalItemsCount", totalCount);
this.set("hasItems", items && items.length > 0);
this.set("hasNoItems", !items || items.length === 0);
this.set("hasMoreItems", items.length < totalCount);
this.pendingRequests--;
if (this.pendingRequests <= 0) {
var self = this;
self.set("isBusy", false);
this.pendingRequests = 0;
}
this.trigger("itemsChanged");
}
}
);
var view = Sitecore.Definitions.Views.ComponentView.extend(
{
listen: _.extend({}, Sitecore.Definitions.Views.ComponentView.prototype.listen, {
"refresh:$this": "refresh",
"next:$this": "next"
}),
initialize: function (options) {
this._super();
var pageIndex, pageSize, fields;
pageSize = parseInt(this.$el.attr("data-sc-pagesize"), 10) || 0;
this.model.set("pageSize", pageSize);
pageIndex = parseInt(this.$el.attr("data-sc-pageindex"), 10) || 0;
this.model.set("pageIndex", pageIndex);
if (this.$el.is("[data-sc-fields]")) {
fields = $.parseJSON(this.$el.attr("data-sc-fields"));
this.model.set("fields", fields);
}
else {
this.model.set("fields", null);
}
this.model.set("language", this.$el.attr("data-sc-language"));
this.model.set("database", this.$el.attr("data-sc-database") || "core");
this.model.set("facetsRootItemId", this.$el.attr("data-sc-facets-root-id"));
this.model.set("formatting", this.$el.attr("data-sc-formatting"));
this.model.set("sorting", this.$el.attr("data-sc-sorting"));
this.model.set("rootItemId", this.$el.attr("data-sc-root-id"));
this.model.set("text", this.$el.attr("data-sc-text") || "");
this.model.set("searchConfig", this.$el.attr("data-sc-searchconfig"));
this.model.set("showHiddenItems", this.$el.data("sc-showhiddenitems"));
this.model.set("pagingMode", this.$el.attr("data-sc-pagingmode") || "appending"); // or paged
this.model.isReady = true;
},
afterRender: function () {
this.refresh();
},
refresh: function () {
this.model.refresh();
},
next: function () {
this.model.next();
}
}
);
Sitecore.Factories.createComponent("CustomSearchDataSource", model, view, "script[type='text/x-sitecore-customsearchdatasource']");
});
I changed the getItems() and the getFacets() methods and renamed of the component in Sitecore.Factories.createComponent("CustomSearchDataSource", model, view, "script[type='text/x-sitecore-customsearchdatasource']"); line.
In the getItems() method I am calling a service url that returns the search results. The service uses the same business and data layers as the rest of the site to retrieve search results from Solr. The only difference between the service method that returns the search results for the Ajax driven pages and the method that the Brochure Builder SPEAK application uses is the returned format.
Here is the code for the Search Api Controller for the SPEAK
application.
public class SearchApiController : Controller
{
public virtual JsonResult Get()
{
var result = new SearchResult() { statusCode = 200, result = new Result() { items = new List(), facets = new List() } };
NameValueCollection querystring = new NameValueCollection(Context.Request.QueryString);
try
{
querystring.Remove("facetsRootItemId");
querystring.Remove("root");
var globalSearchCriteria = ModelService.GenerateSearchCriteria(querystring);
var correctedSpelling = string.Empty;
var facetsRootItemId = Context.Request.QueryString["facetsRootItemId"];
Item facetFolder = null;
if (!string.IsNullOrEmpty(facetsRootItemId))
{
facetFolder = Factory.GetDatabase("core").GetItem(facetsRootItemId);
}
var searchResults = SearchManager.SearchForBrochure(globalSearchCriteria, facetFolder, Sitecore.Context.Item);
var items = new List();
if (searchResults.Hits.Any())
{
result.result = new Result()
{
totalCount = searchResults.TotalSearchResults,
resultCount = searchResults.Hits.Count(),
facets = GetFacets(searchResults.Facets, facetFolder)
};
foreach (var res in searchResults.Hits)
{
var sitecoreItem = res.Document.GetItem();
if (sitecoreItem != null)
{
var customItem = ItemFactory.GetCustomItem(sitecoreItem);
if (customItem is PersonItem)
{
var item = (PersonItem)customItem;
items.Add(new SearchResultItem()
{
ID = item.ID.ToString(),
Name = item.PreferredFullName ?? item.InnerItem.DisplayName,
Icon = item.Image.MediaItem != null ? MediaManager.GetThumbnailUrl(item.Image.MediaItem) : GetIcon(item),
MediaUrl = item.Image.MediaItem != null ? MediaManager.GetMediaUrl(item.Image.MediaItem) : GetIcon(item),
LongID = item.InnerItem.Paths.LongID,
Path = item.InnerItem.Paths.FullPath,
Template = item.InnerItem.Template.InnerItem.Paths.ParentPath,
TemplateId = item.InnerItem.TemplateID.ToString(),
TemplateName = item.InnerItem.TemplateName,
Language = item.InnerItem.Language.Name,
Version = item.InnerItem.Version.Number,
Url = item.Url,
Category = "Person",
Database = item.Database.Name,
DisplayName = item.InnerItem.DisplayName,
HasChildren = item.InnerItem.HasChildren,
itemId = item.ID.ToString()
});
}
else if (customItem is OfficeItem)
{
var item = (OfficeItem)customItem;
items.Add(new SearchResultItem()
{
ID = item.ID.ToString(),
Name = item.OfficeName,
Icon = item.Image.MediaItem != null ? MediaManager.GetThumbnailUrl(item.Image.MediaItem) : GetIcon(item),
MediaUrl = item.Image.MediaItem != null ? MediaManager.GetMediaUrl(item.Image.MediaItem) : GetIcon(item),
LongID = item.InnerItem.Paths.LongID,
Path = item.InnerItem.Paths.FullPath,
Template = item.InnerItem.Template.InnerItem.Paths.ParentPath,
TemplateId = item.InnerItem.TemplateID.ToString(),
TemplateName = item.InnerItem.TemplateName,
Language = item.InnerItem.Language.Name,
Version = item.InnerItem.Version.Number,
Url = item.Url,
Category = "Office",
Database = item.Database.Name,
DisplayName = item.InnerItem.DisplayName,
HasChildren = item.InnerItem.HasChildren,
itemId = item.ID.ToString()
});
}
else if (customItem is IArticle)
{
var item = (CustomItem)customItem;
var articleItem = (IArticle)customItem;
items.Add(new SearchResultItem()
{
ID = item.ID.ToString(),
Name = articleItem.Title,
Icon = GetIcon(item),
LongID = item.InnerItem.Paths.LongID,
Path = item.InnerItem.Paths.FullPath,
Template = item.InnerItem.Template.InnerItem.Paths.ParentPath,
TemplateId = item.InnerItem.TemplateID.ToString(),
TemplateName = item.InnerItem.TemplateName,
Language = item.InnerItem.Language.Name,
Version = item.InnerItem.Version.Number,
Url = articleItem.Url,
Category = "Article",
Database = item.Database.Name,
DisplayName = item.InnerItem.DisplayName,
HasChildren = item.InnerItem.HasChildren,
itemId = item.ID.ToString()
});
}
else if (customItem is IServiceLanding)
{
var item = (CustomItem)customItem;
var experienceItem = (IServiceLanding)customItem;
items.Add(new SearchResultItem()
{
ID = item.ID.ToString(),
Name = experienceItem.Title,
Icon = GetIcon(item),
LongID = item.InnerItem.Paths.LongID,
Path = item.InnerItem.Paths.FullPath,
Template = item.InnerItem.Template.InnerItem.Paths.ParentPath,
TemplateId = item.InnerItem.TemplateID.ToString(),
TemplateName = item.InnerItem.TemplateName,
Language = item.InnerItem.Language.Name,
Version = item.InnerItem.Version.Number,
Url = experienceItem.Url,
Category = "Experience",
Database = item.Database.Name,
DisplayName = item.InnerItem.DisplayName,
HasChildren = item.InnerItem.HasChildren,
itemId = item.ID.ToString()
});
}
}
}
}
result.result.items = items;
return Json(result, JsonRequestBehavior.AllowGet);
}
catch
{
result.statusCode = 500;
return Json(result, JsonRequestBehavior.AllowGet);
}
}
///
/// Gets the facets.
///
/// The facet results.
///
private IEnumerable GetFacets(Sitecore.ContentSearch.Linq.FacetResults facetResults, Item facetFolder = null)
{
var facets = new List();
if (facetResults != null && facetResults.Categories != null)
{
foreach (var facet in facetResults.Categories)
{
if (facetFolder != null && facetFolder.Children.Any())
{
facets.Add(new FacetItem() { Name = facetFolder.Children.FirstOrDefault(f => f["FieldName"] == facet.Name).DisplayName, Values = GetFacetValues(facet.Values) });
}
else
{
facets.Add(new FacetItem() { Name = "Template", Values = GetFacetValues(facet.Values) });
}
}
}
return facets;
}
/// /// Gets the facet values.
/// /// The list.
///
private IEnumerable GetFacetValues(List list)
{
var facetValues = new List();
if (list != null && list.Any())
{
foreach (var itm in list)
{
if (itm.AggregateCount > 0)
{
var item = Factory.GetDatabase("master").GetItem(ShortID.Parse(itm.Name).ToID());
if (item != null)
{
facetValues.Add(new FacetValueItem() { DisplayText = item.DisplayName, Count = itm.AggregateCount, Text = item.Name, Value = itm.Name });
}
}
}
}
return facetValues;
}
/// /// Gets the icon.
/// /// The item.
///
private static string GetIcon(CustomItem item)
{
return Themes.MapTheme(item.InnerItem.Appearance.Icon);
}
}
To be able to use the
ListControl
rendering to display results from the
CustomSearchDataSource
I need to match the service response format to the format that
Sitecore Item Api
returns. For that purpose I have
added the following classes to the solution to generate proper response format.
public class SearchResult
{
public int statusCode { get; set; }
public Result result { get; set; }
}
public class Result
{
public int totalCount { get; set; }
public int resultCount { get; set; }
public IEnumerable items { get; set; }
public IEnumerable facets { get; set; }
}
public class SearchResultItem
{
public string Category { get; set; }
public string Database { get; set; }
public string DisplayName { get; set; }
public bool HasChildren { get; set; }
public string ID { get; set; }
public string itemId { get; set; }
public string Language { get; set; }
public string LongID { get; set; }
public string MediaUrl { get; set; }
public string Icon { get; set; }
public string Name { get; set; }
public string Path { get; set; }
public string Template { get; set; }
public string TemplateId { get; set; }
public string TemplateName { get; set; }
public string Url { get; set; }
public int Version { get; set; }
public string[] Fields { get; set; }
}
public class FacetItem
{
public string Name { get; set; }
public IEnumerable Values { get; set; }
}
public class FacetValueItem
{
public int Count { get; set; }
public string DisplayText { get; set; }
public string Text { get; set; }
public int Priority { get; set; }
public string Value { get; set; }
}
Now I can add my
CustomSearchDataSource
rendering to the layout settings in my application to
Page.Body
placeholder.
Next step is to point newly created rendering to the
CustomSearchDataSource item under
PageSettings
as a
Data Source item:
After that I need to update the
Data
Source
property in the
CustomSearchDataSource
rendering properties.
To display search results on the page I need to add a
ListControl to the list of renderings.
The search results table will have three columns, for that
I’m adding three column items:
IconField,
ItemName and
Template.
In each I need to specify a field name and for icon field I have to enter
format in which the output should be rendered.
Formatted
IconField Column
When you choose to use
Html Template to format the
output of your column,
DataField
field value
must
be empty. In
Html Template field for the
IconField
column I’m going to enter
<img src=”{{Icon}}” /> where Icon matches the object property in the service
results.
"items": [
{
"Category":"Person",
"Database":"web",
"DisplayName":"Aabha Reddy",
"HasChildren":true,
"ID":"{C492C197-60DB-456F-BECB-04E0478E7D56}",
. . .
"Icon":"~/media/3754d6ab6dc94a02a881301480191623.ashx",
"Name":"Aabha Sharma",
"Path":"[item path]",
. . .
},
Below is the screenshot of the
IconField item.
The next column is the
ItemName one. I need to update the
DataField filed on
this item with “Name” value to make it display item name.
I also want to display the item type in the search results
column, so user can easily identify it. For that I’m going to add a
Template column that would display item’s
TemplateName property value.
Now I need to create the
Data
Source item for the
CustomSearchDataSource
rendering. It needs to be based on
SearchDataSource
Parameters template and contain
configurations settings like
page size,
page index,
language,
database, etc.
Once data source item is created, I can update the
Data Source property for the
ResultsListControl rendering.
I need to also specify
Scrollbar Behavior,
ViewMode to be
DetailList
and
Items in
Data Binding section to
have a value of
{Binding DataSource.Items} to
bind my
ListControl with
CustomSearchDataSource I created earlier.
To show the status of the search results returning progress
I am going to add
ProgressIndicator rendering
to the
DialogContent.Main placeholder, the same placeholder where the
ResultsListControl is located. In the rendering
properties windows I need to specify that
Delay is 0, bind it to
DataSource.IsBusy
property, set
AutoShow and
AutoShowTimeout values to 0 and enter
ResultsListControl in the
Target Control field like it is shown below.
To define the facets for the search results, I am adding a
Facets
folder under the
PageSettings and as a child of this folder item, I’m
going to add a
Template
item that is based on
Facet template. In the
FieldName field I need to enter
_template to indicate that the facets should be
based on the item template.
The next step is to add the
FilterControl rendering with the name of
ContentFilterControl.
I am going to place it into
DialogContent.Navigation placeholder to
display it on the left rail in of the application window. In the
Facets
field, I am going to specify
{Binding
DataSource.Facets}
to bind this control to the
Data Source
control’s
Facets property.
The
SearchTextBox doesn’t
have any special properties except for the placeholder where it lives in. I
added it to the
Search.Searches placeholder to make it show up above the
ResultListControl.
Update
CustomSearchDataSource
to use facets and the value from the search text box.
Now as I have
Facets data
source item,
ContentFilterControl and
SearchTextBox renderings configured, I can go back
to the
CustomSearchDataSource rendering and
update its
SelectedFacets and
Text search properties.
In the
FacetsRootitemId I’ll
enter the
ID for the
Facets folder I created earlier.
SelectedFacets I am binding to the
ContentFilerControl.SelectedFacets,
Text to
SearchTextBox.Text
property.
To enable paging for the
ResultsListControl,
I need to add a
Show More button below the
list of results.
First, I’m going to add
Border type rendering to
DialogContent.Main placeholder and name it
PagerBorder.
In the rendering properties I’m going to specify
Content
Alignment
to be “Center” and remove the border (uncheck
ShowBorder).
Then I’m going to add a
Button to the
PagerBorder.Content placeholder with the name of
PagerButton.
In the rendering properties window I am going to set the
following property values:
When
CustomSearchDataSource service doesn’t return any items, I need to display
“No items found”
message. For that I am going to create a folder called
Messages under
PageSettings.
As a child of
Messages item I’ll create an item based on
Notification Message template and call it
No items found.
Now I need to add a
MessageBar
rendering to the
DialogContent.Main placeholder. I’ll call it the
MessageBar.
In rendering properties window for the newly created
rendering I am going to enter the
No items found item’s ID to
Messages field and
{Binding
DataSource.HasNoItems}
to
IsVisible one.
Now when
CustomDataSource doesn't
have any items to show, I’ll have the “No items found” message displayed.
Most of the pieces seem to be in place, now we can check how
the page looks like.
Not bad. I get my data returned and displayed,
Facets
are also showing up. But there are a few things missing. First, the
SearchTextBox doesn’t have a search button to
trigger the search. Let’s add that in.
I am adding an
IconButton with
the name of
SearchIconButton to the
Search.Searches
placeholder and specifing the following properties in the rendering properties window:
- ImageUrl: /sitecore/client/~/media/C2D806C76EA94DEE940AE949D9AFD77A.ashx
- BackgroundPosition: -47px 0
- Tooltip: Search
- Click: javascript:app.DataSource.refresh() that would trigger refreshing of the CustomDataSource
control passing the value from SearchTextBox to
be searched for.
Now if we refresh the application page, you’ll see that
there is a button with a magnifying glass next to the search input box. If I
enter search string into that box and click on the new button, the data
refreshes and displays the relevant content.
Notice that the facets on the left also reflect the change
in data and display new counts for each template. Try checking one of the facet
checkboxes. The list of search result items should change to display only items
that are based on the template you have selected.
I’m going to change the
Navigation title above the
facets to say “Filter Search Results” and remove
Action control from the left rail to make it look even better.
Let’s refresh the page.
Everything seems to be in place. I can search the content
and filter the search results.
Paging also works. Now I can move on to
creating a rendering that would list all items that have been selected to be
included into
Brochure and trigger the PDF generation.
To create a title above the
ResultsListControl,
I added a
Text rendering to the
Search.Filters
placeholder with the name
ItemListText and
specified the following properties in its rendering properties window:
Next
step is adding a new control that would display all selected for brochure items
where user can reorder and delete any of them. This control is the data source
for the list of items to be included in the brochure.
First I am going to add a
Text rendering to the
DialogContent.Navigation placeholder and name it
SeletedTitle.
In
Text field of this
rendering I am going to add “Selected for Brochure” and in
Type –
Divider.
IsVisible property needs to be set to
True.
There is no control that would have the functionality I need
for the
SelectedItems
rendering, so I have
to create a new one. Because it is a control that lists items, I’ll call it
SelectedItemsListControl. In Visual Studio I need
to navigate to the directory where the source files to be located, right
clicked and chose
“Add > New Item”. I’ll choose
“SPEAK Component with
JavaScript” in the templates and enter “
SelectedItemsListControl” in the file
name field.
In the next dialog window I need to specify the location
where I would like the new control item to be created.
As a result there will be two files created in my solution:
SelectedItemsListControl item was also added to the
sitecore tree under
PageSettings in the
Brochure Builder application.
To make my custom control work properly and provide custom rendering
properties, I need to create a template for control parameters. Under
SelectedItemsListControl
item I am going to add a
new
Template item and call it
Control Parameters. The next step is switching to
Sitecore content editor to assign
ConstrolBase
base template to the
Control Parameters template
item. Next step is to return to Visual Studio and open up the
Control Parameters template for editing. I need to
add new template section called
Selected Items
and an Items field of
Single-Line text type. When you edit a template in
Sitecore Rocks, you see a
Build button next
to each field. To the left of that field there is an input box for the field
source. To make a template field show up in a
Selected
Items
section in rendering properties window, I need to assign the
bindmode=read to the source field. To enter that
value you can either type it in or click on the
Build
button then select edit
Bind Mode (Speak) and
then choose appropriate binding. Having
Standard
Values
created wouldn’t hurt either, so I am going to create that too.
After that work is done I can go back to
SelectedItemsListControl item under
PageSettings
and change the
Parameters Template field
value to point to the
Control Parameters template
I just created.
The next step is assigning of the
SelectedItemsListControl rendering to the page.
Below is the rendering properties window with
SelectedItemsListControl1 settings, default and
custom (Notice “Selected Items” section).
In the
.cshtml file I am
going to add the following code:
@using Sitecore.Mvc
@using Sitecore.Mvc.Presentation
@using Sitecore.Web.UI.Controls.Common.UserControls
@model RenderingModel
@{
var userControl = Html.Sitecore().Controls().GetUserControl(Model.Rendering);
userControl.Class = "sc-selecteditemslistcontrol";
userControl.DataBind = "visible: isVisible";
userControl.Requires.Script("client", "selecteditemslistcontrol.js");
var text = userControl.GetString("TextBinding", "Text");
var htmlAttributes = userControl.HtmlAttributes;
}
In the
.js file I will have the following:
define(["sitecore", "knockout"], function (_sc, ko) {
var model = _sc.Definitions.Models.ControlModel.extend({
initialize: function () {
this._super();
this.set("selectedItems", []);
this.set("items", []);
this.on("change:selectedItems", this.changeSelectedItems, this);
this.viewModel.selectedItemList = new ko.observableArray(null);
this.updating = false;
},
changeSelectedItems: function () {
if (arguments.length > 0) {
this.updateSelectedItems(arguments[0]);
} else {
this.set("items", this.get("selectedItems"));
}
},
updateSelectedItems: function (newItem) {
var selectedItems = this.get("items");
var selectedItemList = [];
var newItemModel = newItem.viewModel;
var alreadyExists = false;
var newId = newItemModel.ID();
_.each(selectedItems, function (item) {
if (newId == item.ID()) {
alreadyExists = true;
}
selectedItemList.push(item);
}, this);
if (!alreadyExists) {
selectedItemList.push(newItemModel);
}
this.viewModel.selectedItemList(selectedItemList);
this.set("items", selectedItemList);
},
removeItem: function (id) {
var selectedItems = this.get("items");
selectedItems = _.reject(selectedItems, function (i) {
return i.ID() === id;
}, this);
this.updating = true;
this.viewModel.selectedItemList(selectedItems);
this.set("items", selectedItems);
},
moveUp: function (id) {
var selectedItems = this.get("items");
var itemIndex;
_.each(selectedItems, function (item, idx) {
if (item.ID() === id) {
itemIndex = idx;
}
}, this);
if (itemIndex - 1 >= 0) {
var temp = selectedItems[itemIndex - 1];
selectedItems[itemIndex - 1] = selectedItems[itemIndex];
selectedItems[itemIndex] = temp;
}
this.updating = true;
this.viewModel.selectedItemList(selectedItems);
this.set("items", selectedItems);
},
moveDown: function (id) {
var selectedItems = this.get("items");
var itemIndex;
_.each(selectedItems, function (item, idx) {
if (item.ID() === id) {
itemIndex = idx;
}
}, this);
if (itemIndex + 1 < selectedItems.length) {
var temp = selectedItems[itemIndex + 1];
selectedItems[itemIndex + 1] = selectedItems[itemIndex];
selectedItems[itemIndex] = temp;
}
this.updating = true;
this.viewModel.selectedItemList(selectedItems);
this.set("items", selectedItems);
}
});
var view = _sc.Definitions.Views.ControlView.extend({
initialize: function () {
this._super();
},
removeItem: function (data, event) {
var itemId = data.ID();
this.model.removeItem(itemId);
},
moveUp: function (data, event) {
var itemId = data.ID();
this.model.moveUp(itemId);
},
moveDown: function (data, event) {
var itemId = data.ID();
this.model.moveDown(itemId);
}
});
_sc.Factories.createComponent("SelectedItemsListControl", model, view, ".sc-selecteditemslistcontrol");
});
Each time an item is clicked on in
ResultsListControl, the same item should be added
to the list of items in
SelectedItemsListControl.
To make that happen I am going to add a
Rule rendering
to the
Page.Body placeholder and a data source item for it called
ResultsListControlRule based on the
RuleDefinition template to
PageSettings.
In the
Rules field of
ResultsListControlRule item I will enter the
following conditions and actions:
Custom Action: CallComponentFunction
The first
condition and action in the
Rule 1 and
Rule 2
are out-of-the-box ones, but the
“call component function with parameters”
is a custom rule called
CallComponentFunction
I had to create under
/sitecore/client/Speak/Layouts/Renderings/Resources/Rule/Rules/Actions.
The value I entered in the
Text field is:
call component [targetControlId,,,name] function
[functionName,,,functionName] with parameters [sourceControlId,,,name]
[sourceProperty,,,sourceProperty]
I also added a
.js file to implement the action
functionality. It had to be placed in
/sitecore/shell/client/Speak/Layouts/Renderings/Resources/Rules/ConditionsAndActions/Actions
folder and named
CallComponentFunction.js.
Below is the code for this .js file.
define([], function () {
var action = function (context, args) {
var targetControl = context.app[args.targetControlId],
functionName = args.functionName,
sourceControl = context.app[args.sourceControlId],
selectedItemsPropertyName = "selectedItem",
sourceProperty = args.sourceProperty,
selectedItem,
sourceValue;
if (targetControl == null) {
throw "targetControl not found";
}
if (!functionName) {
throw "functionName is not set";
}
if (sourceControl.get(selectedItemsPropertyName) &&
"attributes" in sourceControl.get(selectedItemsPropertyName)) {
selectedItem = sourceControl.get(selectedItemsPropertyName);
sourceValue = sourceControl.get(selectedItemsPropertyName).attributes[sourceProperty];
} else {
console.debug("Unable to get the property to set");
return;
}
console.log(functionName);
targetControl.trigger(functionName, selectedItem);
};
return action;
});
Now back to the rule rendering. In the properties for the
newly created
ResultsListControlRule I need
to update the
RuleItemId property to point to
the
RuleDefinition item I created under
PageSettings.
The
TargetControl property was updated to
ResultsListControl and the
Trigger one to
change:selectedItem.
When I refresh the page and click on any item in the search
results, I see the same item being added to the
Selected for Brochure
and
Build Button gets enabled.
Looks great, but a few things are still missing. I need to
be able to provide a way for end user to enter the title for my brochure, and
once brochure is built; user should have a way to open created
pdf file.
To provide for end user a way to enter a book title for the
brochure, I am going to add two renderings to my application presentation
settings: a
Text and a
TextBox.
First one is the
Text rendering
with the name of
BookTitleLabel. I need to
add it to the
FirstRowBorderSpan10.Content placeholder and enter “Book
Title: “ into the
Text field.
The second rendering is the
TextBox one with the name of
BookTitleTextBox. It
should be added to the same placeholder as the
Text
rendering. Below you can see the properties for this rendering.
The next big piece of functionality that needs to be implemented
is the
BuildButton
click event handling and
providing a way to open up created by
Sitecore PMX module pdf file. How
PXM modules is used to create PDF Brochures is a subject for another
conversation, and if you are interested in that, you can check out my blog post
on PXM at
http://sitecoreexperiences.blogspot.com/2013/07/sitecore-and-aps-saga-configurations.html
to see how that part is done.
The next rendering to be added is a
ProgressIndicator that would be displayed while the
brochure is being built. I am going to name it
BuildProgressIndicator
and assign it to
Page.Body placeholder.
To process the
click event
I need to add a
Rule rendering called
BuildButtonRule to the
Page.Body
placeholder. I’ll also add a
RuleDefinition item
called
BuildButtonRuleDefinition under
PageSettings
and enter the following condition/action combination into the
Rules field:
This rule essentially turns the visibility of the
BuildProgressIndicator control to
True.
In the rendering properties for the
BuildButtonRule rendering the following values need
to be added:
Notice
RuleItemId,
TargetControl and
Trigger
property values.
The next rule that I need to add to the
Page.Body
placeholder is the
BuildIsDoneRule. This rule
is responsible for the actions that are performed upon completion of the PDF
build. As with any Rule I have to create a
RuleDefinition item under
PageSettings and
specify the following rules:
Below are the properties for the
BuildIsDoneRule rule rendering. Notice
Target Control,
RuleItemId and
Trigger
field values.
Trigger Control property points
to
BrochureBuilderObserver control you’ll see
in the next section.
If the build fails, I need to provide the user with a message
about it. To accomplish that, I am going to add a
Rule rendering to
Page.Body placeholder with the
name of
BuildFailedRule. The rendering needs
a
RuleDefinition, so I’m going to add a
BuildFailedRuleDefinition item to the
PageSettings
one in my application tree. The
BuildFailedRuleDefinition
item should have the following condition and actions in the
Rules field:
Now I need to update the properties for the
BuildFailedRule rendering. There are three fields
that require changing:
- RuleItemId
- Target Control
- Trigger
The screenshot below shows the values that need to be set in
these fields.
With all rules and
ProgressIndicator in place I can now create a control that will be responsible for calling the
service to trigger PDF build and initiate rules execution upon success or
failure of that build. For that purpose I’m going to create a new custom
control called
BrochureBuildObserver. I’m
going to add this control to my solution under
/sitecore/shell/client/Speak/Layouts/Renderings/Data/.
When Visual Studio asks for the location of the Sitecore
item for the control, I am going to specify the
PageSettings item. As a
result Sitecore will create an item that you can see below:
Under
BrochureBuilderObserver item that was just created I am going to add a new
Template
with the name of
Parameters. This template
must have
ControlBase as a base template, so
I’ll switch to Sitecore Content Editor and assign that base template to the
Parameters template. Once that is done, I can go
back to Sitecore Rocks and start editing the template. I need to add three
custom properties that would be available in rendering properties window:
- BookTitle – Text field value from the BookTitleTextBox control. I’ll be able to specify the binding in the properties windows between BookTitle property of BrochureBuildeObserver and the Text property of the BookTitleTextBox;
- SelectedItems – it will hold the binding between SelectedItemsListControl.Items and this property;
- ProgressIndicatorControl – name of the ProgressIndicator control that should become visible and spread over entire page once the build has started. Technically the BuildButtonRule will take care of visibility of this control, but I couldn't get the ProgressIndicator spread over the whole page, hence the property.
All three fields should have
bindmode=readwrite value in the source field to become visible in the rendering properties
window.
The next step is to update the
Parameters template field in the
BrochureBuilderObserver item under
PageSettings
and point it to the
Parameters template item. When that is done, it is time to add the
BrochureBuilderObserver to the list of page renderings.
Below are the property settings for the new
BrochureBuilderObserver. Notice the custom section
with
BookTitle,
SelectedItems
and
ProgressIndicatorControl properites.
The last step is to update the control code to read the
custom properties, execute the build action and provide event handler for the
DownloadButton
.
In the JavaScript file enter the following code:
define(["sitecore"], function (Sitecore) {
"use strict";
var model = Sitecore.Definitions.Models.ComponentModel.extend(
{
initialize: function () {
this._super();
this.set("brochureUrl", "");
this.set("error", "");
this.set("booktitle", "");
this.set("selecteditems", "");
this.set("progressindicatorcontrol", "");
},
build: function () {
var app = this.viewModel.app;
var progressIndicatorControl = this.attributes["progressindicatorcontrol"];
var title = this.attributes["booktitle"];
var selectedItems = this.attributes["selecteditems"];
var progressIndicator = app[progressIndicatorControl];
if (progressIndicator != null) {
progressIndicator.viewModel.height("100%");
}
var ids = [];
_.each(selectedItems, function (item) {
ids.push(item.ID());
}, this);
var idString = ids.join(",");
jQuery.ajax({
type: "GET",
dataType: "json",
url: "/api/sitecore/Print/Brochure",
data: { ids: idString, title: title },
context: this,
cache: false,
success: function (data) {
this.set("brochureUrl", data);
},
error: function (data) {
this.set("error", data);
console.log("There was an error. Try again please!");
}
});
},
openBrochure: function () {
var url = this.get("brochureUrl");
if (url) {
window.open(url);
} else {
console.log("Brochure url is empty");
}
}
});
var view = Sitecore.Definitions.Views.ComponentView.extend(
{
initialize: function () {
this._super();
this.model.set("progressindicatorcontrol", this.$el.data("sc-progressindicatorcontrol"));
}
});
Sitecore.Factories.createComponent("BrochureBuilderObserver", model, view, "script[type='text/x-sitecore-brochurebuilderobserver']");
});
The .cshtml file should have the following:
@using Sitecore.Mvc
@using Weil.SC.Client.Speak.UI.Controls.BrochureBuilderObserver
@model Sitecore.Mvc.Presentation.RenderingModel
@Html.Sitecore().Controls().BrochureBuilderObserver(this.Model.Rendering)
As you can see from the code above the view outputs the control by rendering a C# class. The name of that class is
BrochureBuilderObserver. Below you can see the source code for it.
public class BrochureBuilderObserver : ComponentBase
{
public string BookTitle { get; set; }
public string SelectedItems { get; set; }
public string ProgressIndicatorControl { get; set; }
protected string BookTitleBinding { get; set; }
protected string SelectedItemsBinding { get; set; }
public BrochureBuilderObserver()
{
this.Requires.Script("controls", "brochurebuilderobserver.js");
}
public BrochureBuilderObserver(RenderingParametersResolver parametersResolver)
: base(parametersResolver)
{
Assert.ArgumentNotNull((object)parametersResolver, "parametersResolver");
this.Requires.Script("controls", "brochurebuilderobserver.js");
this.BookTitle = parametersResolver.GetString("BookTitle", "booktitle");
this.SelectedItems = parametersResolver.GetString("SelectedItems", "selecteditems");
this.ProgressIndicatorControl = parametersResolver.GetString("ProgressIndicatorControl", "progressindicatorcontrol");
this.BookTitleBinding = parametersResolver.GetString("BookTitleBinding");
this.SelectedItemsBinding = parametersResolver.GetString("SelectedItemsBinding");
}
protected override void PreRender()
{
base.PreRender();
if (!string.IsNullOrEmpty(this.ProgressIndicatorControl))
this.SetAttribute("data-sc-progressindicatorcontrol", this.ProgressIndicatorControl);
this.SetAttribute(HtmlTextWriterAttribute.Type, "text/x-sitecore-brochurebuilderobserver");
if (!string.IsNullOrEmpty(this.BookTitleBinding))
this.AddBinding("booktitle", this.BookTitleBinding);
if (!string.IsNullOrEmpty(this.SelectedItemsBinding))
this.AddBinding("selecteditems", this.SelectedItemsBinding);
}
protected override void Render(HtmlTextWriter output)
{
this.AddAttributes(output);
output.RenderBeginTag(HtmlTextWriterTag.Script);
output.RenderEndTag();
}
}
I am also going to add an MVC helper for this control:
public static class ControlsExtension
{
public static HtmlString BrochureBuilderObserver(this Sitecore.Mvc.Controls controls, Sitecore.Mvc.Presentation.Rendering rendering)
{
Assert.ArgumentNotNull((object)controls, "controls");
Assert.ArgumentNotNull((object)rendering, "rendering");
return new HtmlString(new BrochureBuilderObserver(controls.GetParametersResolver(rendering)).Render());
}
public static HtmlString CustomSearchDataSource(this Sitecore.Mvc.Controls controls, string controlId, object parameters = null)
{
Assert.ArgumentNotNull((object)controls, "controls");
Assert.ArgumentNotNull((object)controlId, "controlId");
BrochureBuilderObserver control = new BrochureBuilderObserver((RenderingParametersResolver)controls.GetParametersResolver(parameters));
control.ControlId = controlId;
return new HtmlString(control.Render());
}
}
To make sure
Cancel button
at the top of the screen closes the window when clicked, I will add a
CancelButtonRule rendering to the
Page.Body
placeholder. The rule definition item with the name
CancelButtonRuleDefinition
for this rule was is added under
PageSettings.
In the
Rules field of the
CancelButtonRuleDefinition item I am going to add:
In the properties window I will update
RuleItemId,
TargetControl
and
Trigger fields.
After all necessary renderings are added to the page, you
should have page layout settings look like this:
When application page is loaded, you should be able to see
the list of items returned from the controller, search through the content,
filter search results using facets, select items for brochure, build brochure
PDF (if you have PXM module installed and configured) and download generated
PDF file. If everything works, you will see three screens that are shown below:
This concludes this overview of
Brochure Building SPEAK
application creation process. I hope you have found it useful. Happy SPEAKing!