+1-888-365-2779
Try Now
More in this section
Categories
Bloggers
Blogs RSS feed

Creating Sitefinity 4 Content Modules Part 4: Frontend Controls

by Josh Morales

Table of Contents

This series covering Sitefinity Content-Based Module creation is broken up into 5 parts:

Overview

Now that we've created the backend administration for our module, we need to implement the public controls for the frontend. Just as built-in module controls have controls like NewsView, EventsView, etc., we'll develop our own LocationsView control for displaying content items on the public website.

Content Views

We'll define the actual LocationsView control later, but first we need to define the actual views that that control will contain. We need one for the master list, and another for the single item details view.

Both of these controls inherit from ViewBase and make use of the new Virtual Path Provider support for loading embedded templates.

Let's begin by implementing the master list view control.

Master List View

The Master List View is loaded by our LocationsView control (which we'll build last) to display the list of published items on the website. It inherits from ViewBase and requires that we override the InitializeControls method.

Add the class file MasterListView.cs to the Web > UI > Public folder with the following code.

class MasterListView : ViewBase
    
{
    /// <summary>
    ///
Initializes the controls.
  
/// </summary>
    /// <param name="container">
The controls container.</param>
    /// <param name="definition">
The content view definition.</param>
  
protected override void InitializeControls(GenericContainer container, IContentViewDefinition definition)
    {
        // TODO: Implement this method    }
}

Master List View Template

Before we initialize the control lets override the properties that identify the template for our control. Now that we are using the Virtual Path Provider, we no longer need to use the LayoutTemplateName property (return null instead), and can specify a virtual path to the template instead.

Add the following code to your class. Note the virtual path set in the LayoutTemplatePath matches the namespace/folder path to the User Control template we’ll be creating later.

 #region Override Template Properties

        /// <summary>
        /// Gets the name of the embedded layout template.
        /// </summary>
        /// <value></value>
        /// <remarks>
        /// No longer used; replaced with new Virtual Path Provider. Returns null.
        /// </remarks>
        protected override string LayoutTemplateName
        {
            get { return null; }
        }

        /// <summary>
        /// Gets or sets the layout template path.
        /// </summary>
        /// <value>
        /// The layout template path.
        /// </value>
        public override string LayoutTemplatePath
        {
            get
            {
                var path = "~/LocationTemplates/LocationsModule.Web.UI.Public.Resources.MasterListView.ascx";
                return path;
            }
            set
            {
                base.LayoutTemplatePath = value;
            }
        }

        #endregion

Control References

Since the control and its template are separated until runtime, we need a way for the view control to access the controls (such as RadListView, Pager, etc) that are defined in the template. This is done by adding control references to the code. This allows us to use the GetControl method to retrieve these controls at runtime and reference them as part of the control.

In the list view we have only two controls: the RadListView and the Pager. Add the following code to your class to allow access to these controls.

 #region Control References

        /// <summary>
        /// Gets the repeater for Location Items list.
        /// </summary>
        /// <value>The repeater.</value>
        protected internal virtual RadListView LocationsListControl
        {
            get
            {
                return this.Container.GetControl<RadListView>("LocationsList", true);
            }
        }

        /// <summary>
        /// Gets the pager.
        /// </summary>
        /// <value>The pager.</value>
        protected internal virtual Pager Pager
        {
            get
            {
                return this.Container.GetControl<Pager>("pager", true);
            }
        }

        #endregion

Finally, we need to define the template itself. This is simply an .ascx file that contains the markup and controls for the public view of the control.

Any markup, scripts, etc. for handling the layout of your template should be defined here. Remember to match the IDs of the Control References used in the previous code snippet.

Important Note: Be sure to set the Build Action for the control to be “Embedded Resource” so that the templates are compiled into the assembly.

Sitefinity-4-Modules-Embedded-Resource

Here is the template we're using for the Locations Module. Add it in the Web > Public > UI > Resources folder.

<%@ Control Language="C#" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI.ContentUI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="telerik" Namespace="Telerik.Web.UI" Assembly="Telerik.Web.UI" %>

<h1>Locations</h1>

<telerik:RadListView ID="LocationsList" ItemPlaceholderID="ItemsContainer" GroupPlaceholderID="GroupContainer" GroupItemCount="3" runat="server" EnableEmbeddedSkins="false" EnableEmbeddedBaseStylesheet="false">
    <LayoutTemplate>
        <div class="Locations">
            <asp:PlaceHolder ID="GroupContainer" runat="server" />
        </div>
    </LayoutTemplate>
    <GroupTemplate>
        <div class="row">
            <asp:PlaceHolder ID="ItemsContainer" runat="server" />
        </div>
    </GroupTemplate>
    <ItemTemplate>
        <div class="column-small">
            <div class="block">
                <p><sf:DetailsViewHyperLink ID="DetailsViewHyperLink1" runat="server"><div id="photoDiv" runat="server"></div></sf:DetailsViewHyperLink></p>
                <h3><sf:FieldListView ID="Title" runat="server" Text="{0}" Properties="Title" /></h3>
                <address>
                    <sf:FieldListView ID="Address" runat="server" Text="{0}" Properties="Address" /><br />
                    
                    <sf:FieldListView ID="City" runat="server" Text="{0}" Properties="City" />
                    <sf:FieldListView ID="Region" runat="server" Text="{0}" Properties="Region" /><br />
                    <sf:FieldListView ID="PostalCode" runat="server" Text="{0}" Properties="PostalCode" />
                </address>
            </div>
        </div>
    </ItemTemplate>
</telerik:RadListView>

<sf:Pager id="pager" runat="server" />

Now that all the plumbing is in place, we can override the InitializeControls method to retrieve and display the items, as well as adding support for paging results. The following code completes the control for the public list view.

 #region Methods

        /// <summary>
        /// Initializes the controls.
        /// </summary>
        /// <param name="container">The controls container.</param>
        /// <param name="definition">The content view definition.</param>
        protected override void InitializeControls(GenericContainer container, IContentViewDefinition definition)
        {
            // ensure a valid definition is passed
            var masterDefinition = definition as IContentViewMasterDefinition;
            if (masterDefinition == null) return;

            // retrieve locations from the manager
            var manager = LocationsManager.GetManager(this.Host.ControlDefinition.ProviderName);
            var query = manager.GetLocations();

            // check for filters on the locations query
            if (masterDefinition.AllowUrlQueries.HasValue && masterDefinition.AllowUrlQueries.Value)
            {
                query = this.EvaluateUrl(query, "Date", "PublicationDate", this.Host.UrlEvaluationMode, this.Host.UrlKeyPrefix);
                query = this.EvaluateUrl(query, "Author", "Owner", this.Host.UrlEvaluationMode, this.Host.UrlKeyPrefix);
                query = this.EvaluateUrl(query, "Taxonomy", "", typeof(LocationItem), this.Host.UrlEvaluationMode, this.Host.UrlKeyPrefix);
            }

            // modify pager based on query results
            int? totalCount = 0;
            int? itemsToSkip = 0;
            if (masterDefinition.AllowPaging.HasValue && masterDefinition.AllowPaging.Value)
                itemsToSkip = this.GetItemsToSkipCount(masterDefinition.ItemsPerPage, this.Host.UrlEvaluationMode, this.Host.UrlKeyPrefix);

            // culture for Urls in pager
            CultureInfo uiCulture = null;
            if (Config.Get<ResourcesConfig>().Multilingual)
                uiCulture = System.Globalization.CultureInfo.CurrentUICulture;

            // check for additional filters set by the definition
            var filterExpression = DefinitionsHelper.GetFilterExpression(masterDefinition);

            // modify the query with everything from above
            query = Telerik.Sitefinity.Data.DataProviderBase.SetExpressions(
                query,
                filterExpression,
                masterDefinition.SortExpression,
                uiCulture,
                itemsToSkip,
                masterDefinition.ItemsPerPage,
                ref totalCount);
            this.IsEmptyView = (totalCount == 0);

            // display results
            if (totalCount == 0)
                this.LocationsListControl.Visible = false;
            else
            {
                this.ConfigurePager(totalCount.Value, masterDefinition);
                this.LocationsListControl.DataSource = query.ToList();
            }
        }


        /// <summary>
        /// Configures the pager.
        /// </summary>
        /// <param name="vrtualItemCount">The virtual item count.</param>
        /// <param name="masterDefinition">The master definition.</param>
        protected virtual void ConfigurePager(int virtualItemCount, IContentViewMasterDefinition masterDefinition)
        {
            if (masterDefinition.AllowPaging.HasValue &&
                masterDefinition.AllowPaging.Value &&
                masterDefinition.ItemsPerPage.GetValueOrDefault() > 0)
            {
                this.Pager.VirtualItemCount = virtualItemCount;
                this.Pager.PageSize = masterDefinition.ItemsPerPage.Value;
                this.Pager.QueryParamKey = this.Host.UrlKeyPrefix;
            }
            else
            {
                this.Pager.Visible = false;
            }
        }

        #endregion

Details View

The other view needed by our LocationsView control is for loading the “details” view of module data. This will also inherit from ViewBase and is implemented similarly to the master list view.

The only difference is that there is no Pager control to reference, and this time we only bind to a single detail item. Here is the complete implementation for the DetailsView control.

class DetailsView : ViewBase
{
    #region Override Template Properties

    /// <summary>
    /// Gets the name of the embedded layout template.
    /// </summary>
    /// <value></value>
    /// <remarks>
    /// No longer used; replaced with new Virtual Path Provider. Returns null.
    /// </remarks>
    protected override string LayoutTemplateName
    {
        get { return null; }
    }

    /// <summary>
    /// Gets or sets the layout template path.
    /// </summary>
    /// <value>
    /// The layout template path.
    /// </value>
    public override string LayoutTemplatePath
    {
        get
        {
            var path = "~/LocationTemplates/LocationsModule.Web.UI.Public.Resources.DetailsView.ascx";
            return path;
        }
        set
        {
            base.LayoutTemplatePath = value;
        }
    }
    #endregion

    #region Control References

    /// <summary>
    /// Gets the repeater for news list.
    /// </summary>
    /// <value>The repeater.</value>
    protected internal virtual RadListView DetailsViewControl
    {
        get
        {
            return this.Container.GetControl<RadListView>("DetailsView", true);
        }
    }

    #endregion

    #region Overridden methods

    /// <summary>
    /// Initializes the controls.
    /// </summary>
    /// <param name="container">The controls container.</param>
    /// <param name="definition">The content view definition.</param>
    protected override void InitializeControls(GenericContainer container, IContentViewDefinition definition)
    {
        // ensure a valid definition is passed
        var detailDefinition = definition as IContentViewDetailDefinition;
        if (detailDefinition == null) return;

        // retrieve item from host control
        var locationsView = (LocationsView)this.Host;
        var item = locationsView.DetailItem as LocationItem;
        if (item == null)
        {
            // no item
            if (this.IsDesignMode())
            {
                this.Controls.Clear();
                this.Controls.Add(new LiteralControl("A location item was not selected or has been deleted. Please select another one."));
            }
            return;
        }

        // show item details
        this.DetailsViewControl.DataSource = new LocationItem[] { item };
    }

    #endregion
}

The Details View also needs its own template to render the individual location.

<%@ Control Language="C#" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI.ContentUI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="sf" Namespace="Telerik.Sitefinity.Web.UI" Assembly="Telerik.Sitefinity" %>
<%@ Register TagPrefix="telerik" Namespace="Telerik.Web.UI" Assembly="Telerik.Web.UI" %>

<div class="locations-single content">

    <telerik:RadListView ID="DetailsView" ItemPlaceholderID="ItemsContainer" runat="server" EnableEmbeddedSkins="false" EnableEmbeddedBaseStylesheet="false">
        <LayoutTemplate>
            <asp:PlaceHolder ID="ItemsContainer" runat="server" />
        </LayoutTemplate>
        <ItemTemplate>
            <div class="column-small">
                <div class="block">
                    <p id="photo" runat="server"></p>
                    <h3><sf:FieldListView ID="Title" runat="server" Text="{0}" Properties="Title" /></h3>
                    <address>
                        <sf:FieldListView ID="Address" runat="server" Text="{0}" Properties="Address" /><br />
                        <sf:FieldListView ID="City" runat="server" Text="{0}" Properties="City" /> 
                        <sf:FieldListView ID="Region" runat="server" Text="{0}" Properties="Region" /><br />
                        <sf:FieldListView ID="PostalCode" runat="server" Text="{0}" Properties="PostalCode" /><br />
                        
                        
                    </address>
                </div>
            </div>
        </ItemTemplate>
    </telerik:RadListView>
</div>

LocationsView

With our backing views in place, we can now develop the LocationsView control. This is a simple implementation; all we need to do is inherit from ContentView and override some properties so that they reference the views we just created, as defined in our LocationsDefinitions class.

We'll add these definitions in a moment, but first here is the complete code for the LocationsView, which we’ll place in the Web > UI > Public folder.

[RequireScriptManager]
class LocationsView : ContentView
{
    public override string ModuleName
    {
        get
        {
            if (String.IsNullOrEmpty(base.ControlDefinitionName)) return "Locations";
            return base.ModuleName;
        }
        set
        {
            base.ModuleName = value;
        }
    }

    /// <summary>
    /// Gets or sets the name of the configuration definition for the whole control. From this definition
    /// control can find out all other configurations needed in order to construct views.
    /// </summary>
    /// <value>The name of the control definition.</value>
    public override string ControlDefinitionName
    {
        get
        {
            if (String.IsNullOrEmpty(base.ControlDefinitionName))
                return LocationsDefinitions.FrontendDefinitionName;
            return base.ControlDefinitionName;
        }
        set
        {
            base.ControlDefinitionName = value;
        }
    }

    /// <summary>
    /// Gets or sets the name of the master view to be loaded when
    /// control is in the ContentViewDisplayMode.Master
    /// </summary>
    /// <value></value>
    public override string MasterViewName
    {
        get
        {
            if (!String.IsNullOrEmpty(base.MasterViewName))
                return base.MasterViewName;

            return LocationsDefinitions.FrontendListViewName;
        }
        set
        {
            base.MasterViewName = value;
        }
    }

    /// <summary>
    /// Gets or sets the name of the detail view to be loaded when
    /// control is in the ContentViewDisplayMode.Detail
    /// </summary>
    /// <value></value>
    public override string DetailViewName
    {
        get
        {
            if (!String.IsNullOrEmpty(base.DetailViewName))
                return base.DetailViewName;

            return LocationsDefinitions.FrontendDetailViewName;
        }
        set
        {
            base.DetailViewName = value;
        }
    }

    /// <summary>
    /// Gets or sets the text to be shown when the box in the designer is empty
    /// </summary>
    /// <value></value>
    public override string EmptyLinkText
    {
        get
        {
            return "Edit";
        }
    }
}

Update Frontend Definitions

As mentioned in the last section, we need to add the definitions for the frontend controls so that Sitefinity can wire everything up internally.

Just like the definitions we created for the backend administration, these are simply instantiating the controls and wiring them together using the named constants we defined previously in the #region Constants section of the LocationsDefintions class.

Simply add the following code to the LocationsDefinitions class to complete the implementation of the frontend controls.

 #region Frontend ContentView

        /// <summary>
        /// Defines the ContentView control for News on the frontend
        /// </summary>
        /// <param name="parent">The parent configuration element.</param>
        /// <returns>A configured instance of <see cref="ContentViewControlElement"/>.</returns>
        internal static ContentViewControlElement DefineLocationsFrontendContentView(ConfigElement parent)
        {
            // define content view control
            var controlDefinition = new ContentViewControlElement(parent)
            {
                ControlDefinitionName = LocationsDefinitions.FrontendDefinitionName,
                ContentType = typeof(LocationItem)
            };

            // *** define views ***

            #region Locations List View

            // define element
            var LocationsListView = new ContentViewMasterElement(controlDefinition.ViewsConfig)
            {
                ViewName = LocationsDefinitions.FrontendListViewName,
                ViewType = typeof(MasterListView),
                AllowPaging = true,
                DisplayMode = FieldDisplayMode.Read,
                ItemsPerPage = 6,
                FilterExpression = DefinitionsHelper.PublishedOrScheduledFilterExpression,
                SortExpression = "Title ASC"
            };

            // add to content view
            controlDefinition.ViewsConfig.Add(LocationsListView);

            #endregion

            #region Locations Details View

            // Initialize View
            var newsDetailsView = new ContentViewDetailElement(controlDefinition.ViewsConfig)
            {
                ViewName = LocationsDefinitions.FrontendDetailViewName,
                ViewType = typeof(DetailsView),
                ShowSections = false,
                DisplayMode = FieldDisplayMode.Read
            };

            // add to ContentView
            controlDefinition.ViewsConfig.Add(newsDetailsView);

            #endregion

            // return content view control
            return controlDefinition;
        }

        #endregion

For a complete listing of the LocationsDefinitions class, see this snippet or download the example project belo.

Register Frontend Controls

The last thing we need to do for this post is register the frontend controls for our module in the LocationsConfig class so they are loaded with the module in Sitefinity.

Simply modify the InitializeDefaultViews method by adding the following line to complete this part of the module's creation.

// add frontend views to configuration
contentViewControls.Add(LocationsDefinitions.DefineLocationsFrontendContentView(contentViewControls));

What's Next

At this point, all the pieces are in place for our Locations Module. We have defined the data layer and access, and support both administration in the backend as well as displaying items on the public website.

All that is left to do is handle the installation and registration of our module in Sitefinity. This will be the subject of our next post. Be sure to download the module up to this point, available below to compare your progress.

If you have any questions, comments, or suggestions, be sure to visit our Sitefinity 4 SDK Discussion Forum.

Downloads

1 comment

Leave a comment
  1. Jonathan Oct 10, 2012
    Where does the "LocationsViewDesigner" come into play? I note that, in the full download at the end of this tutorial series, there is a class called LocationsModule.Web.UI.Public.Designers.LocationsViewDesigner, but there is no mention of it in the tutorial itself.

    I made the mistake of following through these tutorials and building up the solution, expecting it to actually work at the end... which it doesn't. I note that (for example) this class is missing, and wonder what other differences there are / if this is the reason the solution fails.

    I have now downloaded the 5.1sp1 sdk and looked at the Locations module included with that. It builds, installs, and seems to work fine; but the code differs in some significant ways (eg LocationsModule.LocationsModule.InstallPages()), and I'm left confused wondering why...

    Leave a comment