Extend a Dynamic Module Widget Designer with your own properties

Extend a Dynamic Module Widget Designer with your own properties

Background information

For a project that I was working on recently I needed to extend some default behaviors of the Dynamic Module Widget. These widgets are generated by the Dynamic Module Builder and help you to quickly create fully functional modules on the fly. Of course, when you need specific functionality in a widget, you need to dive into the details.

If you develop a normal, stand-alone widget, it is quite easy to work with your own properties. Like if you want the user to enter some text that is used for displaying a title on the frontend of the website, you can just define a string property. Sitefinity will make sure that this property ends up in the Advanced Settings of your widgets designer, like you can see in the following example, where I have defined some properties that hold Guid's of some pages inside the CMS:

Advanced properties Sitefinity widget

With the help of Sitefinity Thunder or just your own code, you can create designer windows for these widget, that shows these properties in a more user-friendly way, like I have done here:

Page selectors in Sitefintiy designers

What about Dynamic Module Designers?

So the widgets that the Dynamic Module Builder generates, also comes a designer and out of the box, it is quite difficult to extend these widgets with your own properties and even better: your own designer.

The Use Case

Of course, some Use Case came up and I needed to dive in this problem to find a solution. So what we need was indeed to have some way to store Page ID's with the Dynamic Module Widget.

This website has a Dynamic Module for Projects. These projects are visible on an overview page like shown below:

Dynamic Module Builder in Sitefinity

Whenever a visitor clicks on a specific project, it will be redirected to a page that shows the project details.

Dynamic Module Builder in Sitefinity

On this detail page there are some links that will redirect the visitor to other pages. The first two links are coming from a Taxonomy field out of our Dynamic Module, the third link is one of those Page ID fields (Guid[]). Clicking on these links will redirect to specific pages with or without some extra parameters.

The solution

So first we need to drop the idea that we can use the generated widgets for this. We have to come up with our own implementation of the Dynamic Module widget. Of course don't have to rewrite it from scratch, since we can inherit from those classes.

We need to follow the next steps to create such implementation:

  1. Create our own DynamicContentView
  2. Create our own DynamicContentViewDesigner
  3. Create our own CustomContentViewDesigner
  4. Link it all together
  5. Add the widget to the Toolbox
  6. Use the properties in your code

That sounds scary, right? But it is quite straightforward.

1. Create our own DynamicContentView

First thing we need to do is to create our ProjectsDynamicContentView which will be replacing the generated Dynamic Content Widget. This is also the class / control that will be added to the Toolbox.

The code for this class you can find below:

public class ProjectsDynamicContentView : DynamicContentView {
 
        public Guid PageIdDonations { get; set; }
        public Guid PageIdThemes { get; set; }
        public Guid PageIdCountries { get; set; }
        public Guid PageIdRegions { get; set; }
 
        protected override void InitializeMasterView() {
 
            if (HasValidRelatedDataConfiguration && RelatedItemsIds != null) {
                MasterViewControl.SourceItemsIds = RelatedItemsIds;
            }
 
            DynamicContentViewMaster masterViewControl = new ProjectsMasterListView();
            var str = (string.IsNullOrEmpty(MasterViewDefinition.TemplateKey) ? DefaultMasterTemplateKey : MasterViewDefinition.TemplateKey);
 
            masterViewControl.TemplateKey = str;
            masterViewControl.DynamicContentType = DynamicContentType;
            masterViewControl.MasterViewDefinition = MasterViewDefinition;
            masterViewControl.UrlEvaluationMode = UrlEvaluationMode;
            masterViewControl.UrlKeyPrefix = UrlKeyPrefix;
            masterViewControl.Host = this;
 
            Controls.Add(masterViewControl);
        }

What we actually do is that we use all the built-in functionality of the DynamicContentView, but in this example we also tell Sitefinity to use a specific MasterListView, instead of the default one. Regarding the topic of this blogpost, I'm not going into that.

What we see is that I have defined the four custom properties that I needed.

2. Create our own DynamicContentViewDesigner

Now that we have the DynamicContentView, we need to implement it's designer. If we do nothing at this point, the default designer of the Dynamic Content Widget will show up.

We need to create a class named ProjectsDynamicContentViewDesigner that inherits from DynamicContentViewDesigner, so that we can extend it to our needs. You can find the code for this class below:

ProjectsDynamicContentViewDesigner.cs

public class ProjectsDynamicContentViewDesigner : DynamicContentViewDesigner {
 
        protected override void AddViews(Dictionary<string, ControlDesignerView> views) {
 
            base.AddViews(views);
 
            var projectsCustomContentViewDesigner = new ProjectsCustomContentViewDesigner();
            views.Add(projectsCustomContentViewDesigner.ViewName, projectsCustomContentViewDesigner);
        }
    }

In here you see that we override the AddViews method to add a new View to the designer. This new view is also a new class that inherits from ContentViewDesignerView.

3. Create our own CustomContentViewDesigner

The code for that class you can see below. I did not inserted all the code, since I defined 4 PageSelectors, so in the example I use just one PageSelector.

ProjectsCustomContentViewDesigner.cs

public class ProjectsCustomContentViewDesigner : ContentViewDesignerView {
 
        #region Control References
        /// <summary>
        /// Gets the page selector control.
        /// </summary>
        /// <value>The page selector control.</value>
        protected internal virtual PagesSelector PageSelectorPageIdDonations {
            get {
                return Container.GetControl<PagesSelector>("pageSelectorPageIdDonations", true);
            }
        }
 
        /// <summary>
        /// Gets the selector tag.
        /// </summary>
        /// <value>The selector tag.</value>
        public HtmlGenericControl SelectorTagPageIdDonations {
            get {
                return Container.GetControl<HtmlGenericControl>("selectorTagPageIdDonations", true);
            }
        }
 
        #endregion
 
        #region Properties
 
        public string UiCulture {
            get {
                if (_uiCulture != null) return _uiCulture;
                _uiCulture = SystemManager.CurrentHttpContext.Request.QueryString["uiCulture"];
 
                if (_uiCulture != null) return _uiCulture;
                var settings = SystemManager.CurrentContext.AppSettings;
                if (!settings.Multilingual) return _uiCulture;
                if (String.IsNullOrEmpty(_uiCulture)) {
                    _uiCulture = settings.DefaultFrontendLanguage.Name;
                }
                return _uiCulture;
            }
            set {
                _uiCulture = value;
            }
        }
 
        public override string ViewName {
            get { return "CustomSettings"; }
        }
 
        public override string ViewTitle {
            get { return "Custom Settings"; }
        }
 
        /// <summary>
        /// Gets the name of the embedded layout template.
        /// </summary>
        /// <remarks>
        /// Override this property to change the embedded template to be used with the dialog
        /// </remarks>
        protected override string LayoutTemplateName {
            get {
                return null;
            }
        }
 
        /// <summary>
        /// Gets or sets the path of the external template to be used by the control.
        /// </summary>
        /// <value></value>
        public override string LayoutTemplatePath {
            get {
                return "~/Widgets/Projects/ContentViewDesigners/ProjectsCustomContentViewDesigner.ascx";
            }
            set {
                base.LayoutTemplatePath = value;
            }
        }
        #endregion
 
        protected override void InitializeControls(GenericContainer container) {
 
            // Set root node for page selector
            PageSelectorPageIdDonations.RootNodeID = SiteInitializer.CurrentFrontendRootNodeId;
            PageSelectorPageIdDonations.UICulture = UiCulture;
 
        }
 
        public override IEnumerable<ScriptDescriptor> GetScriptDescriptors() {
 
            var scriptDescriptors = new ScriptControlDescriptor(GetType().FullName, ClientID);
 
            scriptDescriptors.AddComponentProperty("pageSelectorPageIdDonations", PageSelectorPageIdDonations.ClientID);
            scriptDescriptors.AddElementProperty("selectorTagPageIdDonations", SelectorTagPageIdDonations.ClientID);
            scriptDescriptors.AddProperty("uiCulture", UiCulture);
 
            return new[] { scriptDescriptors };
        }
 
        public override IEnumerable<ScriptReference> GetScriptReferences() {
 
            var res = PageManager.GetScriptReferences(ScriptRef.JQuery);
            var telerikAssemblyName = typeof(TextField).Assembly.GetName().FullName;
 
            res.Add(new ScriptReference("Telerik.Sitefinity.Web.UI.ControlDesign.Scripts.IDesignerViewControl.js", telerikAssemblyName));
            res.Add(new ScriptReference("~/Widgets/Projects/ContentViewDesigners/ProjectsCustomContentViewDesigner.js"));
            return res;
        }
 
        private string _uiCulture;
    }

From this point it is actually the same approach when you create designers for your custom UserControls. It has a class, a template and a javascript file.

ProjectsCustomContentViewDesigner.ascx

<%@ Control %>
<%@ Register Assembly="Telerik.Sitefinity" TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI" %>
<%@ Register Assembly="Telerik.Sitefinity" TagPrefix="sitefinity" Namespace="Telerik.Sitefinity.Web.UI" %>
 
<sitefinity:ResourceLinks ID="resourcesLinks" runat="server">
    <sitefinity:ResourceFile Name="Styles/Ajax.css" />
    <sitefinity:ResourceFile Name="Styles/jQuery/jquery.ui.core.css" />
    <sitefinity:ResourceFile Name="Styles/jQuery/jquery.ui.dialog.css" />
    <sitefinity:ResourceFile Name="Styles/jQuery/jquery.ui.theme.sitefinity.css" />
</sitefinity:ResourceLinks>
<div id="designerLayoutRoot" class="sfContentViews sfSingleContentView" style="max-height: 400px; overflow: auto;">
    <ol>
        <li class="sfFormCtrl">
            <label class="sfTxtLbl" for="selectedPageIdDonationsLabel">Donations page</label>
            <span style="display: none;" class="sfSelectedItem" id="selectedPageIdDonationsLabel">
                <asp:Literal ID="Literal1" runat="server" Text="" />
            </span>
            <span class="sfLinkBtn sfChange">
                <a href="javascript: void(0)" class="sfLinkBtnIn" id="pageSelectButtonPageIdDonations">
                    <asp:Literal ID="Literal2" runat="server" Text="<%$Resources:Labels, SelectDotDotDot %>" />
                </a>
            </span>
            <div id="selectorTagPageIdDonations" runat="server" style="display: none;">
                <sf:PagesSelector runat="server" ID="pageSelectorPageIdDonations"
                    AllowExternalPagesSelection="false" AllowMultipleSelection="false" />
            </div>
            <div class="sfExample">Set the page which holds the donations widget</div>
        </li>
    </ol>
</div>

ProjectsCustomContentViewDesigner.js

/// <reference name="MicrosoftAjax.js"/>
/// <reference name="Telerik.Sitefinity.Resources.Scripts.jquery-1.4.2-vsdoc.js" assembly="Telerik.Sitefinity.Resources"/>
 
Type._registerScript("ProjectsCustomContentViewDesigner.js", ["IDesignerViewControl.js"]);
Type.registerNamespace("SitefinityWebApp.Widgets.Projects.ContentViewDesigners");
 
 
SitefinityWebApp.Widgets.Projects.ContentViewDesigners.ProjectsCustomContentViewDesigner = function (element) {
    SitefinityWebApp.Widgets.Projects.ContentViewDesigners.ProjectsCustomContentViewDesigner.initializeBase(this, [element]);
 
    /* Initialize PageIdDonations fields */
    this._pageSelectorPageIdDonations = null;
    this._selectorTagPageIdDonations = null;
    this._PageIdDonationsDialog = null;
 
    this._showPageSelectorPageIdDonationsDelegate = null;
    this._pageSelectedPageIdDonationsDelegate = null;
 
    this._controldataFieldNameMap = {};
    this._parentDesigner = null;
    this._refreshing = false;
    this._onLoadDelegate = null;
    this._onUnloadDelegate = null;
    this._PageSelectorDonations = null;
    this._PageSelectorCountries = null;
    this._PageSelectorThemes = null;
    this._resizeControlDesignerDelegate = null;
     
    this._uiCulture = null;
};
 
SitefinityWebApp.Widgets.Projects.ContentViewDesigners.ProjectsCustomContentViewDesigner.prototype = {
    initialize: function () {
 
        SitefinityWebApp.Widgets.Projects.ContentViewDesigners.ProjectsCustomContentViewDesigner.callBaseMethod(this, 'initialize');
 
        /* Initialize PageIdDonations */
        this._showPageSelectorPageIdDonationsDelegate = Function.createDelegate(this, this._showPageSelectorPageIdDonationsHandler);
        $addHandler(this.get_pageSelectButtonPageIdDonations(), "click", this._showPageSelectorPageIdDonationsDelegate);
 
        this._pageSelectedPageIdDonationsDelegate = Function.createDelegate(this, this._pageSelectedPageIdDonationsHandler);
        this.get_pageSelectorPageIdDonations().add_doneClientSelection(this._pageSelectedPageIdDonationsDelegate);
         
        var pageSelectorDonations = this.get_pageSelectorPageIdDonations();
        pageSelectorDonations.set_uiCulture(this.get_uiCulture());
 
        if (this._selectorTagPageIdDonations) {
            this._PageIdDonationsDialog = jQuery(this._selectorTagPageIdDonations).dialog({
                autoOpen: false,
                modal: false,
                width: 395,
                closeOnEscape: true,
                resizable: false,
                draggable: false,
                zIndex: 5000
            });
        }
    },
    dispose: function () {
 
        SitefinityWebApp.Widgets.Projects.ContentViewDesigners.ProjectsCustomContentViewDesigner.callBaseMethod(this, 'dispose');
 
        /* Dispose PageIdDonations */
        if (this._showPageSelectorPageIdDonationsDelegate) {
            $removeHandler(this.get_pageSelectButtonPageIdDonations(), "click", this._showPageSelectorPageIdDonationsDelegate);
            delete this._showPageSelectorPageIdDonationsDelegate;
        }
 
        if (this._pageSelectedPageIdDonationsDelegate) {
            this.get_pageSelectorPageIdDonations().remove_doneClientSelection(this._pageSelectedPageIdDonationsDelegate);
            delete this._pageSelectedPageIdDonationsDelegate;
        }
    },
 
    findElement: function (id) {
        var result = jQuery(this.get_element()).find("#" + id).get(0);
        return result;
    },
     
 
 
    /* --------------------------------- public methods --------------------------------- */
 
    refreshUI: function () {
 
        var controlData = this.get_controlData();
 
        /* RefreshUI PageIdDonations */
        if (controlData.PageIdDonations && controlData.PageIdDonations !== "00000000-0000-0000-0000-000000000000") {
            var pagesSelectorPageIdDonations = this.get_pageSelectorPageIdDonations().get_pageSelector();
            var selectedPageLabelPageIdDonations = this.get_selectedPageIdDonationsLabel();
            var selectedPageButtonPageIdDonations = this.get_pageSelectButtonPageIdDonations();
            pagesSelectorPageIdDonations.add_selectionApplied(function (o, args) {
                var selectedPage = pagesSelectorPageIdDonations.get_selectedItem();
                if (selectedPage) {
                    selectedPageLabelPageIdDonations.innerHTML = selectedPage.Title.Value;
                    jQuery(selectedPageLabelPageIdDonations).show();
                    selectedPageButtonPageIdDonations.innerHTML = '<span>Change</span>';
                }
            });
            pagesSelectorPageIdDonations.set_selectedItems([{ Id: controlData.PageIdDonations }]);
        }
    },
 
    applyChanges: function () {
         
    },
     
    /* PageIdDonations private methods */
    _showPageSelectorPageIdDonationsHandler: function (selectedItem) {
        var controlData = this.get_controlData();
        var pagesSelector = this.get_pageSelectorPageIdDonations().get_pageSelector();
        if (controlData.PageIdDonations) {
            pagesSelector.set_selectedItems([{ Id: controlData.PageIdDonations }]);
        }
        this._PageIdDonationsDialog.dialog("open");
        jQuery("#designerLayoutRoot").hide();
        this._PageIdDonationsDialog.dialog().parent().css("min-width", "355px");
        dialogBase.resizeToContent();
    },
 
    _pageSelectedPageIdDonationsHandler: function (items) {
        var controlData = this.get_controlData();
        var pagesSelector = this.get_pageSelectorPageIdDonations().get_pageSelector();
        this._PageIdDonationsDialog.dialog("close");
        jQuery("#designerLayoutRoot").show();
        dialogBase.resizeToContent();
        if (items == null) {
            return;
        }
        var selectedPage = pagesSelector.get_selectedItem();
        if (selectedPage) {
            this.get_selectedPageIdDonationsLabel().innerHTML = selectedPage.Title.Value;
            jQuery(this.get_selectedPageIdDonationsLabel()).show();
            this.get_pageSelectButtonPageIdDonations().innerHTML = '<span>Change</span>';
            controlData.PageIdDonations = selectedPage.Id;
        }
        else {
            jQuery(this.get_selectedPageIdDonationsLabel()).hide();
            this.get_pageSelectButtonPageIdDonations().innerHTML = '<span>Select...</span>';
            controlData.PageIdDonations = "00000000-0000-0000-0000-000000000000";
        }
    },
 
    /* --------------------------------- properties -------------------------------------- */
 
    /* PageIdDonations properties */
    get_pageSelectButtonPageIdDonations: function () {
        if (this._pageSelectButtonPageIdDonations == null) {
            this._pageSelectButtonPageIdDonations = this.findElement("pageSelectButtonPageIdDonations");
        }
        return this._pageSelectButtonPageIdDonations;
    },
    get_selectedPageIdDonationsLabel: function () {
        if (this._selectedPageIdDonationsLabel == null) {
            this._selectedPageIdDonationsLabel = this.findElement("selectedPageIdDonationsLabel");
        }
        return this._selectedPageIdDonationsLabel;
    },
    get_pageSelectorPageIdDonations: function () {
        return this._pageSelectorPageIdDonations;
    },
    set_pageSelectorPageIdDonations: function (val) {
        this._pageSelectorPageIdDonations = val;
    },
    get_selectorTagPageIdDonations: function () {
        return this._selectorTagPageIdDonations;
    },
    set_selectorTagPageIdDonations: function (value) {
        this._selectorTagPageIdDonations = value;
    },
 
    get_uiCulture: function () {
        return this._uiCulture;
    },
 
    set_uiCulture: function (value) {
        this._uiCulture = value;
    },
 
    // gets the reference to the parent designer control
    get_parentDesigner: function () {
        return this._parentDesigner;
    },
 
    // sets the reference fo the parent designer control
    set_parentDesigner: function (value) {
        this._parentDesigner = value;
    },
 
 
    // gets the name of the currently selected master view name of the content view control
    get_currentViewName: function () {
        return (this._currentViewName) ? this._currentViewName : this.get_controlData().MasterViewName;
    },
 
    // gets the client side representation of the currently selected master view definition
    get_currentView: function () {
        var currentViewName = this.get_currentViewName();
        var data = this.get_controlData();
        var views = data.ControlDefinition.Views;
        if (views.hasOwnProperty(currentViewName)) {
            return views[currentViewName];
        } else {
            var views = data.ControlDefinition.Views;
            for (var v in views) {
                var current = views[v];
                if (current.IsMasterView) {
                    return current;
                }
            }
            return null;
        }
    },
 
    // this fixes the data if there are some incompatible values set in advanced mode
    _adjustControlData: function (data) {
        var view = data.ControlDefinition.Views[this.get_currentViewName()];
        if (!view) {
            var views = data.ControlDefinition.Views;
            var viewName;
            for (var key in views) {
                if (views[key].IsMasterView) {
                    viewName = key;
                    break;
                }
            }
            data.MasterViewName = viewName;
        }
    },
 
    _resolvePropertyPath: function (fieldControl) {
        var viewPath = "ControlDefinition.Views['" + this.get_currentViewName() + "']";
        return viewPath;
    },
 
    // gets the object that represents the client side representation of the control
    // being edited
    get_controlData: function () {
 
        var parent = this.get_parentDesigner();
        if (parent) {
            var pe = parent.get_propertyEditor();
            if (pe) {
                return pe.get_control();
            }
        }
        alert('Control designer cannot find the control properties object!');
    },
 
    // function to initialize resizer methods and handlers
    _resizeControlDesigner: function () {
        setTimeout("dialogBase.resizeToContent()", 100);
    }
};
 
SitefinityWebApp.Widgets.Projects.ContentViewDesigners.ProjectsCustomContentViewDesigner.registerClass('SitefinityWebApp.Widgets.Projects.ContentViewDesigners.ProjectsCustomContentViewDesigner', Sys.UI.Control, Telerik.Sitefinity.Web.UI.ControlDesign.IDesignerViewControl);

I know, it is a lot of code for a simple PageSelector ;)

4. Link it all together

Now that we have all our classes defined, we need to tell the Sitefinity that whenever our custom DynamicContentView is placed on a page, it should not use the default designer, but our custom designer. For that, we need to decorate the ProjectsDynamicContentView class with an attribute:

[ControlDesigner(typeof(ProjectsDynamicContentViewDesigner))]
 public class ProjectsDynamicContentView : DynamicContentView {

This will tell Sitefinity to use our custom DynamicContentViewDesigner.

At this point I created the following classes and files for my Dynamic Module:

Project overview Sitefinity

5. Add the widget to the Toolbox

Last thing we need to do is to add this new ProjectsDynamicContentView in our toolbox. I registered it like this:

<add enabled="True" type="SitefinityWebApp.Widgets.Projects.ProjectsDynamicContentView, SitefinityWebApp" title="Projects overview" cssClass="sfNewsViewIcn" moduleName="Projects" DynamicContentTypeName="Telerik.Sitefinity.DynamicTypes.Model.Projects.Project" DefaultMasterTemplateKey="1fedbd9d-b27d-6052-a3f7-ff00004a31dd" DefaultDetailTemplateKey="20edbd9d-b27d-6052-a3f7-ff00004a31dd" visibilityMode="None" name="Telerik.Sitefinity.DynamicTypes.Model.Projects.Project" />

You can take the code from the generated widget as an example for the registration. You need to change the Type property to refer to the class we just created.

If we now drop the widget onto our page, we can see our designer, with the custom view:

Extended designer Sitefinity

6. Use the properties in your code

In the end I created a new template that I referred to from my Dynamic Content widget. You can use the built-in templates, or define your own. I choose for the last one, since I needed to use some code-behind coding.

During the OnDataBound event, I define the links that you see on the Projects Detail page. In the code I refer to the custom properties:

// First we need to get the project host
var projectsView = this.GetHostControl<ProjectsDynamicContentView>();

// Then we can resolve the page, based on the PageIdCountries property
country.NavigateUrl = string.Concat(projectsView.PageIdCountries.GetPage().GetUrl(), "/", t.Name);

Finally!

So these are the steps you need to take in order to achieve the Use Case as describer above. I didn't verified this with Sitefinity, so there might be an easier way to do this, but for now this works fine.

About this Sitefinity project

LIGHT FOR THE WORLD is an NGO committed to saving eyesight, improving the quality of life and advocating for the rights of persons with disabilities in the underprivileged regions of our world.

Make sure to visit their website for more information: http://www.lightfortheworld.nl

 

If you have questions about this or any other Sitefinity topic, feel free to contact me!

door Daniel Plomp

Reactie

RadEditor - HTML WYSIWYG Editor. MS Word-like content editing experience thanks to a rich set of formatting tools, dropdowns, dialogs, system modules and built-in spell-check.
RadEditor's components - toolbar, content area, modes and modules
   
Toolbar's wrapper 
 
Content area wrapper
RadEditor's bottom area: Design, Html and Preview modes, Statistics module and resize handle.
It contains RadEditor's Modes/views (HTML, Design and Preview), Statistics and Resizer
Editor Mode buttonsStatistics moduleEditor resizer
  
RadEditor's Modules - special tools used to provide extra information such as Tag Inspector, Real Time HTML Viewer, Tag Properties and other.