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.
As you'd probably guess, the following blog post draws inspiration from the constructive client feedback we get through our developer network.
For the purposes of the post we're going to illustrate our explanations with an actual use case scenario:
I have created a custom field for my blog post, where I store specific data. I'd like to be able to filter the displayed list of blog posts on the frontend depending on this field value. The best place to specify this filtering would be in the BlogPosts widget designer, where I can check a CheckBox and filter only the Blog posts that have certain value in their custom field.
Before jumping to the actual implementation, let's have some background.
Sitefinity ContentView controls have their designer specified with a ControlDesigner attribute. For example:
[RequireScriptManager]
[ControlDesigner(
typeof
(BlogsDesigner))]
[PropertyEditorTitle(
typeof
(BlogResources),
"BlogPostViewPropertyEditorTitle"
)]
public
class
BlogPostView : ContentView
Each designer inheriting from ContentViewDesignerBase (the base class for our ContentView designers) has an AddViews() method where we specify which ContentSelectorsDesignerViews the designer will operate with. Inside the AddViews() each view is defined, and then added to the views collection:
using
System.Collections.Generic;
using
System.Web.UI.WebControls;
using
Telerik.Sitefinity.Web.UI.ControlDesign;
using
Telerik.Sitefinity.Blogs.Model;
using
Telerik.Sitefinity.Localization;
using
Telerik.Sitefinity.Modules.Blogs.Web.UI.Public;
namespace
Telerik.Sitefinity.Modules.Blogs.Web.UI.Designers
{
/// <summary>
/// Blogs content view designer control
/// </summary>
public
class
BlogsDesigner : ContentViewDesignerBase
{
/// <inheritdoc />
protected
override
string
ScriptDescriptorTypeName
{
get
{
return
typeof
(ContentViewDesignerBase).FullName;
}
}
/// <summary>
/// Adds the designer views.
/// </summary>
/// <param name="views">The views.</param>
protected
override
void
AddViews(Dictionary<
string
, ControlDesignerView> views)
{
var contentSelectorsSettings =
new
BlogsContentSelectorsDesignerView();
//contentSelectorsSettings.ContentSelector.TitleText = "Select blogs";
//contentSelectorsSettings.ContentSelector.ItemType = typeof(Blog).FullName;
string
providerName = ((BlogPostView)
this
.PropertyEditor.Control).ControlDefinition.ProviderName;
contentSelectorsSettings.ContentSelector.ItemType =
typeof
(BlogPost).FullName;
contentSelectorsSettings.ContentSelector.ProviderName = providerName;
var listSettings =
new
ListSettingsDesignerView();
listSettings.SortItemsText = Res.Get<BlogResources>().SortBlogPosts;
listSettings.DesignedMasterViewType =
typeof
(MasterPostsView).FullName;
var singleItemSettings =
new
SingleItemSettingsDesignerView();
singleItemSettings.DesignedDetailViewType =
typeof
(DetailPostView).FullName;
views.Add(contentSelectorsSettings.ViewName, contentSelectorsSettings);
views.Add(listSettings.ViewName, listSettings);
views.Add(singleItemSettings.ViewName, singleItemSettings);
}
}
}
You are already familiar with the views that I've highlighted above these are the Content, List Settings, and Single item Settings tabs of your News, Blog Posts, etc. designers. For the purposes of this blog post we're going to extend the BlogsContentSelectorsDesignerView which carries out the business logic for providing a user friendly interface for filtering the data source the BlogPosts widget displays.
1. Our first step would be to create a new class that inherits from BlogPostView - this would allow us to decorate our custom BlogPosts widget with a custom designer:
using
System;
using
System.Linq;
using
Telerik.Sitefinity.Modules.Blogs.Web.UI;
using
Telerik.Sitefinity.Web.UI.ControlDesign;
namespace
SitefinityWebApp.CustomWidgets.BlogPosts
{
[ControlDesigner(
typeof
(BlogsDesignerCustom))]
public
class
BlogPostViewCustom : BlogPostView
{
}
}
2. The BlogsDesignerCustom is another class we need to create. It will inherit from BlogsDesigner - the default designer for BlogPosts widget, and will allow us to replace the default BlogsContentSelectorsDesignerView with a new one inside the AddViews() method:
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
Telerik.Sitefinity.Blogs.Model;
using
Telerik.Sitefinity.Localization;
using
Telerik.Sitefinity.Modules.Blogs;
using
Telerik.Sitefinity.Modules.Blogs.Web.UI;
using
Telerik.Sitefinity.Modules.Blogs.Web.UI.Designers;
using
Telerik.Sitefinity.Modules.Blogs.Web.UI.Public;
using
Telerik.Sitefinity.Web.UI.ControlDesign;
namespace
SitefinityWebApp.CustomWidgets.BlogPosts
{
public
class
BlogsDesignerCustom : BlogsDesigner
{
protected
override
string
ScriptDescriptorTypeName
{
get
{
return
typeof
(ContentViewDesignerBase).FullName;
}
}
protected
override
void
AddViews(Dictionary<
string
, ControlDesignerView> views)
{
var contentSelectorsSettings =
new
BlogsContentSelectorsDesignerViewCustom();
string
providerName = ((BlogPostView)
this
.PropertyEditor.Control).ControlDefinition.ProviderName;
contentSelectorsSettings.ContentSelector.ItemType =
typeof
(BlogPost).FullName;
contentSelectorsSettings.ContentSelector.ProviderName = providerName;
var listSettings =
new
ListSettingsDesignerView();
listSettings.SortItemsText = Res.Get<BlogResources>().SortBlogPosts;
listSettings.DesignedMasterViewType =
typeof
(MasterPostsView).FullName;
var singleItemSettings =
new
SingleItemSettingsDesignerView();
singleItemSettings.DesignedDetailViewType =
typeof
(DetailPostView).FullName;
views.Add(contentSelectorsSettings.ViewName, contentSelectorsSettings);
views.Add(listSettings.ViewName, listSettings);
views.Add(singleItemSettings.ViewName, singleItemSettings);
}
}
}
As you can see telling our custom designer to use another class instead of the default BlogsContentSelectorsDesignerView is as easy as declaring the new view - an approach some of you might be already familiar with from the ProductsCatalog sample in our Sitefinity SDK.
3. The BlogsContentSelectorsDesignerViewCustom class we'll create inherits from the base BlogsContentSelectorsDesignerView and will use a custom template (to provide the UI for working with our CheckBox, and a custom client-side component to accommodate for our business logic)
3.1 Creating the template
The template is based entirely on the default template for BlogsContentSelectorsDesignerView, the only difference being a CheckBox control placed on it:
<%@ Register Assembly="Telerik.Web.UI" Namespace="Telerik.Web.UI" TagPrefix="telerik" %>
<%@ Register Assembly="Telerik.Sitefinity" Namespace="Telerik.Sitefinity.Web.UI" TagPrefix="sitefinity" %>
<%@ Register Assembly="Telerik.Sitefinity" TagPrefix="designers" Namespace="Telerik.Sitefinity.Web.UI.ControlDesign" %>
<
sitefinity:ProvidersSelector
ID
=
"providersSelector"
runat
=
"server"
></
sitefinity:ProvidersSelector
>
<
div
class
=
"sfBasicDim"
>
<
div
id
=
"parentSelectorTag"
runat
=
"server"
style
=
"display: none;"
>
<
designers:ContentSelector
ID
=
"parentSelector"
runat
=
"server"
TitleText="<%$Resources:Labels, ChooseBlogs %>"
BindOnLoad="false"
AllowMultipleSelection="true"
WorkMode="List"
SearchBoxTitleText="<%$Resources:Labels, NarrowBlogTitle %>"
ListModeClientTemplate="<
strong
class
=
'sfItemTitle'
>{{Title}}</
strong
><
span
class
=
'sfDate'
>{{PublicationDate ? PublicationDate.sitefinityLocaleFormat('dd MMM yyyy') : ""}}</
span
>"/>
</
div
>
<
div
id
=
"selectorTag"
runat
=
"server"
style
=
"display: none;"
>
<
designers:ContentSelector
ID
=
"selector"
runat
=
"server"
WorkMode
=
"List"
TitleText="<%$Resources:Labels, ChooseBlogPost %>"
BindOnLoad="false"
SearchBoxInnerText=""
SearchBoxTitleText="<%$Resources:Labels, NarrowPostTitle %>"
AllowMultipleSelection="false" />
</
div
>
<
h1
id
=
"choicesTitleHeading"
runat
=
"server"
><
asp:Literal
ID
=
"choicesTitle"
runat
=
"server"
Text="<%$Resources:Labels, WhichBlogPostsToDisplay %>" /></
h1
>
<
div
class
=
"sfContentViews"
>
<
ul
class
=
"sfRadioList sfFormCtrl"
>
<
li
>
<
asp:RadioButton
runat
=
"server"
ID
=
"contentSelect_AllParents"
GroupName
=
"ParentSelection"
Text="<%$Resources:Labels, FromAllBlogs %>" />
</
li
>
<
li
>
<
asp:RadioButton
runat
=
"server"
ID
=
"contentSelect_MultipleParents"
GroupName
=
"ParentSelection"
Text="<%$Resources:Labels, FromSelectedBlogsOnly %>" />
<
div
id
=
"selectorGroup"
>
<
div
class
=
"sfExpandedPropertyDetails"
>
<
asp:Label
ID
=
"selectedParentContentTitle"
runat
=
"server"
Text="<%$Resources:Labels, BlogNotSelected %>" CssClass="sfSelectedItem" />
<
span
class
=
"sfLinkBtn sfChange"
runat
=
"server"
id
=
"btnSelectSingleItemWrapper"
>
<
asp:LinkButton
NavigateUrl
=
"javascript:void(0)"
runat
=
"server"
ID
=
"btnSelectParent"
OnClientClick
=
"return false;"
CssClass
=
"sfLinkBtnIn"
><
asp:Literal
ID
=
"Literal1"
runat
=
"server"
Text="<%$Resources:Labels, SelectBlog %>" /></
asp:LinkButton
>
</
span
>
</
div
>
</
div
>
</
li
>
</
ul
>
<
div
id
=
"narrowSelection"
runat
=
"server"
class
=
"sfExpandableSection"
>
<
asp:CheckBox
Text
=
"IsStaffArticle"
ID
=
"isStaffArticleBox"
runat
=
"server"
/>
<
h3
><
asp:LinkButton
ID
=
"btnNarrowSelection"
OnClientClick
=
"return false;"
runat
=
"server"
Text="<%$Resources:Labels, NarrowSelectionComplementoryText %>" CssClass="sfMoreDetails" /></
h3
>
<
ul
class
=
"sfRadioList sfTargetList"
>
<
li
>
<
asp:RadioButton
runat
=
"server"
ID
=
"contentSelect_AllItems"
Checked
=
"true"
GroupName
=
"ContentSelection"
Text="<%$Resources:Labels, AllPublishedPostsFromSelectedBlogs %>" />
</
li
>
<
li
>
<
asp:RadioButton
runat
=
"server"
ID
=
"contentSelect_OneItem"
GroupName
=
"ContentSelection"
Text="<%$Resources:Labels, OneParticularPostOnly %>" style="display:none"/>
<
div
class
=
"sfExpandedPropertyDetails"
id
=
"selectorPanel"
style
=
"display:none"
>
<
asp:Label
ID
=
"selectedContentTitle"
runat
=
"server"
Text="<%$Resources:Labels, NoSelectedBlogPost %>" CssClass="sfSelectedItem" />
<
asp:LinkButton
NavigateUrl
=
"javascript:void(0)"
runat
=
"server"
ID
=
"btnSelectSingleItem"
OnClientClick
=
"return false;"
CssClass
=
"sfLinkBtn sfChange"
>
<
strong
class
=
"sfLinkBtnIn"
>
<
asp:Literal
ID
=
"btnSelectBlogPostTitleLiteral"
runat
=
"server"
Text="<%$Resources:Labels, SelectBlogPost %>" /></
strong
>
</
asp:LinkButton
>
</
div
>
</
li
>
<
li
>
<
asp:RadioButton
runat
=
"server"
ID
=
"contentSelect_SimpleFilter"
GroupName
=
"ContentSelection"
Text="<%$Resources:Labels, SelectionOfPosts %>" />
<
div
id
=
"selectorsPanel"
>
<
designers:FilterSelector
ID
=
"filterSelector"
runat
=
"server"
AllowMultipleSelection
=
"true"
ItemsContainerTag
=
"ul"
ItemTag
=
"li"
ItemsContainerCssClass
=
"sfCheckListBox sfExpandedPropertyDetails"
DisabledTextCssClass
=
"sfTooltip"
>
<
Items
>
<
designers:FilterSelectorItem
ID
=
"FilterSelectorItem1"
runat
=
"server"
Text="<%$Resources:Labels, ByCategories %>"
GroupLogicalOperator="AND" ItemLogicalOperator="OR" ConditionOperator="Contains"
QueryDataName="Categories" QueryFieldName="Category" QueryFieldType="System.Guid">
<
SelectorResultView
>
<
sitefinity:HierarchicalTaxonSelectorResultView
ID
=
"HierarchicalTaxonSelectorResultView1"
runat
=
"server"
WebServiceUrl
=
"~/Sitefinity/Services/Taxonomies/HierarchicalTaxon.svc"
AllowMultipleSelection
=
"true"
HierarchicalTreeRootBindModeEnabled
=
"false"
BindOnLoad
=
"false"
>
</
sitefinity:HierarchicalTaxonSelectorResultView
>
</
SelectorResultView
>
</
designers:FilterSelectorItem
>
<
designers:FilterSelectorItem
ID
=
"FilterSelectorItem2"
runat
=
"server"
Text="<%$Resources:Labels, ByTags %>"
GroupLogicalOperator="AND" ItemLogicalOperator="OR" ConditionOperator="Contains"
QueryDataName="Tags" QueryFieldName="Tags" QueryFieldType="System.Guid">
<
SelectorResultView
>
<
sitefinity:FlatTaxonSelectorResultView
ID
=
"FlatTaxonSelectorResultView1"
runat
=
"server"
WebServiceUrl
=
"~/Sitefinity/Services/Taxonomies/FlatTaxon.svc"
AllowMultipleSelection
=
"true"
BindOnLoad
=
"false"
>
</
sitefinity:FlatTaxonSelectorResultView
>
</
SelectorResultView
>
</
designers:FilterSelectorItem
>
<
designers:FilterSelectorItem
ID
=
"FilterSelectorItem3"
runat
=
"server"
Text="<%$Resources:Labels, ByDates %>"
GroupLogicalOperator="AND" ItemLogicalOperator="AND"
QueryDataName="Dates" QueryFieldName="PublicationDate" QueryFieldType="System.DateTime"
CollectionTranslatorDelegate="_translateQueryItems"
CollectionBuilderDelegate="_buildQueryItems">
<
SelectorResultView
>
<
sitefinity:DateRangeSelectorResultView
ID
=
"DateRangeSelectorResultView1"
runat
=
"server"
SelectorDateRangesTitle="<%$Resources:Labels, DisplayPostsPublishedIn %>">
</
sitefinity:DateRangeSelectorResultView
>
</
SelectorResultView
>
</
designers:FilterSelectorItem
>
</
Items
>
</
designers:FilterSelector
>
</
div
>
</
li
>
<
li
style
=
"display:none;"
>
<
asp:RadioButton
runat
=
"server"
Enabled
=
"false"
ID
=
"contentSelect_AdvancedFilter"
GroupName
=
"ContentSelection"
Text="<%$Resources:Labels, AdvancedSelection %>" /><
asp:Literal
ID
=
"Literal2"
runat
=
"server"
Text="<%$Resources:Labels, InProcessOfImplementation %>" />
</
li
>
</
ul
>
</
div
>
</
div
>
</
div
>
3.2 Telling our BlogsContentSelectorsDesignerViewCustom to use the new template, and CheckBox
To specify that our custom ContentSelectorsDesignerView will use a template different than the one BlogsContentSelectorsDesignerView uses, we just need to override the LayoutTemplatePath property:
public
override
string
LayoutTemplatePath
{
get
{
return
"~/CustomWidgets/BlogPosts/BlogsContentSelectorsDesignerViewCustomTemplate.ascx"
;
}
set
{
base
.LayoutTemplatePath = value;
}
}
and use Container.GetControl<T> to get our Checkbox:
protected
CheckBox IsStaffArticle
{
get
{
return
Container.GetControl<CheckBox>(
"isStaffArticleBox"
,
true
);
}
}
3.3 Passing the CheckBox to the client component, specifying our custom script
We need to pass a reference to the CheckBox on the client, so we can operate with it and act accordingly to its state. For this purpose we just add it as a new property to the base ScriptControlDescriptor:
public
override
IEnumerable<System.Web.UI.ScriptDescriptor> GetScriptDescriptors()
{
var baseDesc = (ScriptControlDescriptor)
base
.GetScriptDescriptors().Last();
baseDesc.AddProperty(
"isStaffArticle"
,
this
.IsStaffArticle.ClientID);
return
new
[] { baseDesc };
}
Our last job on the BlogsContentSelectorsDesignerViewCustom is to reference our custom script file, where we'll carry the business logic:
public
override
IEnumerable<System.Web.UI.ScriptReference> GetScriptReferences()
{
var scripts =
new
List<ScriptReference>(
base
.GetScriptReferences());
scripts.Add(
new
ScriptReference(BlogsContentSelectorsDesignerViewCustom.controlScript,
typeof
(BlogsContentSelectorsDesignerViewCustom).Assembly.FullName));
return
scripts;
}
private
const
string
controlScript =
"SitefinityWebApp.CustomWidgets.BlogPosts.BlogsContentSelectorsDesignerViewScriptCustom.js"
;
4. Implementing the JavaScript logic
Our last step before achieving the desired filtering effect is to inherit from the base client script, and modify the FilterExpression of the MasterPostsView which displays the list of blog posts.
By overriding the base applyChanges and refreshUI methods inside our custom client script, we can gain control over the MasterPostsView instance on the client, and modify its FilterExpression property depending on whether the checkbox is checked or not:
//overrides of refreshUI and applyChanges
applyChanges:
function
() {
debugger;
//get checkbox
var
myCheckbox = $get(
this
.get_isStaffArticle());
//check value
if
(myCheckbox) {
//get masteview
var
masterView =
this
.get_currentMasterView();
if
(myCheckbox.checked) {
//see if filter already contains IsStaffArticle
if
(masterView.FilterExpression.indexOf(
"IsStaffArticle=True"
) == -1) {
//set the filter expression
var
tempFilter = masterView.FilterExpression.concat(
" AND IsStaffArticle=True"
);
masterView.FilterExpression = tempFilter;
}
}
else
if
(masterView.FilterExpression.indexOf(
"IsStaffArticle=True"
) !== -1) {
//remove from the filter expression
var
tempFilter = masterView.FilterExpression.replace(
" AND IsStaffArticle=True"
,
""
);
masterView.FilterExpression = tempFilter;
}
}
SitefinityWebApp.CustomWidgets.BlogPosts.BlogsContentSelectorsDesignerViewCustom.callBaseMethod(
this
,
"applyChanges"
);
},
refreshUI:
function
() {
debugger;
//get checkbox
var
myCheckbox = $get(
this
.get_isStaffArticle());
//get masteview
var
masterView =
this
.get_currentMasterView();
if
(masterView) {
//see if filterExpression contains IsStaffArticle=True
if
(masterView.FilterExpression.indexOf(
"IsStaffArticle=True"
) !== -1)
//if true, check checkbox
myCheckbox.checked =
true
;
}
SitefinityWebApp.CustomWidgets.BlogPosts.BlogsContentSelectorsDesignerViewCustom.callBaseMethod(
this
,
"refreshUI"
);
},
this.get_currentMasterView() is a method provided by the base ContentSelectorsDesignerView and allows us to operate with the corresponding MasterView and its properties,
and _isStaffArticle is our CheckBox.
The above should wrap up the basic steps necessary for achieving the desired functionality. For your convenience you can find the complete sample uploaded here: BlogPosts.
Please make sure to specify the correct relative path to the template, or change it to be an embedded resource.
I hope this post helps you achieve the desired use case scenarios, knowing how easily extensible this part of Sitefinity is.
Subscribe to get all the news, info and tutorials you need to build better business apps and sites