Table of Contents
This series covering Sitefinity Content-Based Module creation is broken up into 5 parts:
- Part 1: Project Setup and Hello World
- Part 2: Data Layer
- Part 3: Backend Administration
- Part 4: Frontend Controls
- Part 5: Module Installation
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.
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.