Categories
Bloggers
Blogs RSS feed

Extending Sitefinity ContentSelectorsDesignerViews

by Boyan Barnev

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 deisgner 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");
    },
where 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.

4 comments

Leave a comment
  1. Ernest Feb 21, 2013

    Can you please double the sample code provided under the link "here" because I don't think they apply to this post.

    Thank you

  2. Ernest Feb 21, 2013

    double check*

  3. Boyan Feb 22, 2013

    Hello Ernest,

     

    Thank you very much for pointing out the incorrect attachment. 

    The correct files have been uploaded, and linked for you to download.

  4. TREARINEERT Apr 02, 2014
    http://www.njbic.org/online/buytramadol/ order tramadol no prescription - buy tramadol with paypal

    Leave a comment