Creating Sitefinity 4 Content Modules Part 3: Backend Administration

Creating Sitefinity 4 Content Modules Part 3: Backend Administration

Posted on July 13, 2011 0 Comments

The content you're reading is getting on in years
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.

Table of Contents

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

Overview

Now that we have defined the data layer for our custom module, we need to create the user interface for the backend administration. In our next post we'll also create and register the frontend public controls.

This section is a big lengthier than all the others, however a lot of the code you should be able to use as-is, modifying only the areas that pertain to your specific module and data type.

Both the backend and front end controls, as well as many key components of Sitefinity that will interact with your module, make use of Web Services to pass data back and forth. So we'll begin by implementing this service layer, then proceed to creating the backend.

Web Services

Sitefinity web services follow the Model-View-ViewModel (MVVM) pattern, so we begin by implementing the View Model for our data class. Create a new LocationItemViewModel.cs class file in the Web > Services > Data folder.

This class inherits from ContentViewModelBase and is fairly straightforward, simply mapping the LocationItem data properties to the View Model class. Here is the full implementation.

public class LocationItemViewModel : ContentViewModelBase
{
    #region Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="LocationItemViewModel"/> class.
    /// </summary>
    public LocationItemViewModel() : base() { }

    /// <summary>
    /// Initializes a new instance of the <see cref="LocationItemViewModel"/> class.
    /// </summary>
    /// <param name="location">The location item.</param>
    /// <param name="provider">The provider.</param>
    public LocationItemViewModel(LocationItem location, ContentDataProviderBase provider)
        : base(location, provider)
    {
        this.Address = location.Address;
        this.City = location.City;
        this.Region = location.Region;
        this.PostalCode = location.PostalCode;
    }

    #endregion

    #region Public Methods and Overrides

    /// <summary>
    /// Get live version of this.ContentItem using this.provider
    /// </summary>
    /// <returns>
    /// Live version of this.ContentItem
    /// </returns>
    protected override Content GetLive()
    {
        return this.provider.GetLiveBase<LocationItem>((LocationItem)this.ContentItem);
    }

    /// <summary>
    /// Get temp version of this.ContentItem using this.provider
    /// </summary>
    /// <returns>
    /// Temp version of this.ContentItem
    /// </returns>
    protected override Content GetTemp()
    {
        return this.provider.GetTempBase<LocationItem>((LocationItem)this.ContentItem);
    }

    #endregion

    #region Public Properties

    public string Address { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string PostalCode { get; set; }

    #endregion
}

Next, create a new LocationsBackendService.cs class file in the Web > Services folder. This inherits from ContentServiceBase using the LocationItem, LocationItemViewModel, and LocationsManager we defined earlier to wire up everything for accessing the backend.

public class LocationsBackendService : ContentServiceBase<LocationItem, LocationItemViewModel, LocationsManager>
{
    /// <summary>
    /// Gets the content items.
    /// </summary>
    /// <param name="providerName">Name of the provider.</param>
    /// <returns></returns>
    public override IQueryable<LocationItem> GetContentItems(string providerName)
    {
        return this.GetManager(providerName).GetLocations();
    }

    /// <summary>
    /// Gets the child content items.
    /// </summary>
    /// <param name="parentId">The parent id.</param>
    /// <param name="providerName">Name of the provider.</param>
    /// <returns></returns>
    public override IQueryable<LocationItem> GetChildContentItems(Guid parentId, string providerName)
    {
        // TODO: Implement this method
        throw new NotImplementedException();
    }

    /// <summary>
    /// Gets the content item.
    /// </summary>
    /// <param name="id">The id.</param>
    /// <param name="providerName">Name of the provider.</param>
    /// <returns></returns>
    public override LocationItem GetContentItem(Guid id, string providerName)
    {
        return this.GetManager(providerName).GetLocation(id);
    }

    /// <summary>
    /// Gets the parent content item.
    /// </summary>
    /// <param name="id">The id.</param>
    /// <param name="providerName">Name of the provider.</param>
    /// <returns></returns>
    public override LocationItem GetParentContentItem(Guid id, string providerName)
    {
        // TODO: Implement this method
        throw new NotImplementedException();
    }

    /// <summary>
    /// Gets the manager.
    /// </summary>
    /// <param name="providerName">Name of the provider.</param>
    /// <returns></returns>
    public override LocationsManager GetManager(string providerName)
    {
        return LocationsManager.GetManager(providerName);
    }

    /// <summary>
    /// Gets the view model list.
    /// </summary>
    /// <param name="contentList">The content list.</param>
    /// <param name="dataProvider">The data provider.</param>
    /// <returns></returns>
    public override IEnumerable<LocationItemViewModel> GetViewModelList(IEnumerable<LocationItem> contentList, ContentDataProviderBase dataProvider)
    {
        var list = new List<LocationItemViewModel>();

        foreach (var location in contentList)
            list.Add(new LocationItemViewModel(location, dataProvider));

        return list;
    }

We'll register this web service in our final post when we install the module, but for now this is all we need for our web service and we can move on to developing the backend.

Backend Controls

Sitefinity modules have a unified model which allows them to share the same interface for viewing, creating, and editing items in the backend. This is accomplished with a series of controls which we'll be defining for our module's backend.

All of the implementation details, such as item creation, sorting, filtering, previewing, etc., are already handled by Sitefinity. We need to only specify what components our module will support by explicitly defining them in code, then returning those controls wrapped in a ContentView control to our module.

Begin by creating a new LocationsDefinitions.cs class file inside the Web > UI folder using the following code. The constants are used simply for convenience, so we aren't hard coding string literals and thus avoiding errors like spelling mistakes.

public class LocationsDefinitions
{
    #region Constructors

    /// <summary>
    /// Static constructor that makes it impossible to use the definitions
    /// without the module 
    /// </summary>
    static LocationsDefinitions()
    {
        SystemManager.GetApplicationModule(LocationsModule.ModuleName);
    }

    #endregion

    #region Constants

    public const string BackendDefinitionName = "LocationsBackend";
    public const string BackendListViewName = "LocationsBackendListView";
    public const string BackendInsertViewName = "LocationsBackendInsertView";
    public const string BackendEditViewName = "LocationsBackendEditView";
    public const string BackendPreviewName = "LocationsBackendPreview";

    public const string FrontendDefinitionName = "LocationsFrontend";
    public const string FrontendListViewName = "LocationsFrontendListView";
    public const string FrontendDetailViewName = "LocationsDetailView";

    #endregion
}

Backend Content View

Module backend administration is handled through an instance of the ContentViewControlElement class. Through this class you will add all administrative components for the module.

This is done by adding the DefineLocationsBackendContentView method below to your class. Since there are several components required, I've only included an outline of the method below, explaining each of the #regions in more detail below.

/// <summary>
        /// Defines the locations backend content view (control panel and views).
        /// </summary>
        /// <param name="parent">The parent element hosting the backend content view.</param>
        /// <returns></returns>
        public static ContentViewControlElement DefineLocationsBackendContentView(ConfigElement parent)
        {
            // initialize the content view; this is the element that will be returned to the page and holds all views of the admin panel
            var backendContentView = new ContentViewControlElement(parent)
            {
                ControlDefinitionName = BackendDefinitionName,
                ContentType = typeof(LocationItem),
                UseWorkflow = false
            };

            // GridView element serves as the "List View" for the item list. Grid columns are defined later
            var locationsGridView = new MasterGridViewElement(backendContentView.ViewsConfig)
            {
                ViewName = LocationsDefinitions.BackendListViewName,
                ViewType = typeof(MasterGridView),
                AllowPaging = true,
                DisplayMode = FieldDisplayMode.Read,
                ItemsPerPage = 50,
                SearchFields = "Title",
                SortExpression = "Title ASC",
                Title = "Locations",
                WebServiceBaseUrl = "~/Sitefinity/Services/Content/Locations.svc/"
            };
            backendContentView.ViewsConfig.Add(locationsGridView);

            #region Module Main Toolbar definition

            #region Locations Grid (List View)

                    #region Locations Grid Columns

            #region Dialog Window definitions

                    #region Insert Item Dialog

                    #region Edit Item Dialog

                    #region Preview Item Dialog

            #region Admin Forms Views

                    #region Create Item Form View

                    #region Edit Item Form View

                    #region Preview Item Form View

            #region Locations Backend Forms Definitions

                    #region Insert Form

                    #region Edit Form

                    #region Preview Form

            return backendContentView;
        }

The full code for the method is available here in a separate window as well as in the sample project in the Downloads section below, which has the completed module up to the end of this post.

Although the code is lengthy, a significant portion of this is boilerplate, reusable code to copy-and-paste, modifying it fit the needs of your module.

The code is broken up into #regions to better outline the necessary components, which are fairly straightforward and self explanatory, but I'll also take a moment to briefly describe what is happening in each section.

  1. First we define the actual ContentViewControlElement which is the completed backend control
  2. Then the MasterGridViewElement hosts the grid for the “list view” is added to the backend control
  3. #region Module Main Toolbar definition: buttons for “create”, “delete”, etc are defined here
  4. #region Locations Grid (List View): Represents the actual grid for listing items in our module
    1. #region Locations Grid Columns: defines the data columns to bind to the grid
  5. #region Dialog Window definitions: defines dialog windows for different admin modes (insert, edit, preview)
  6. #region Admin Forms Views: form controls (insert, edit, preview) used inside the dialogs created in the previous region
  7. #region Locations Backend Forms Definitions: calls helper methods (see below) that populates the forms created in the previous region with controls that matchup to the data properties (e.g.  TextBox input control for “Title”)

Finally, we return the completed backend control so that it can be used as the Administration tool for the module.

Backend Form Definition Helper Methods

When we reach the #region Locations Backend Forms Definitions in step 7 above, we call the helper method CreateBackendFormToolbar creates the menu on these forms with actions such as “Create”, “Preview” and “Cancel”.

Sitefinity-4-Locations-Module-Toolbar

The toolbar is a straightforward implementation and can be reused with little or no modifications, depending on what options you want available to your users. Add the following method to your class.

 #region Backend Form Toolbar

        /// <summary>
        /// Creates the backend form toolbar.
        /// </summary>
        /// <param name="detailView">The detail view.</param>
        /// <param name="resourceClassId">The resource class id.</param>
        /// <param name="isCreateMode">if set to <c>true</c> [is create mode].</param>
        /// <param name="itemName">Name of the item.</param>
        /// <param name="addRevisionHistory">if set to <c>true</c> [add revision history].</param>
        /// <param name="showPreview">if set to <c>true</c> [show preview].</param>
        /// <param name="backToItems">The back to items.</param>
        private static void CreateBackendFormToolbar(DetailFormViewElement detailView, bool isCreateMode, bool showPreview)
        {
            // create toolbar
            var toolbarSectionElement = new WidgetBarSectionElement(detailView.Toolbar.Sections)
            {
                Name = "BackendForm",
                WrapperTagKey = HtmlTextWriterTag.Div,
                CssClass = "sfWorkflowMenuWrp"
            };

            // Create / Save Command
            toolbarSectionElement.Items.Add(new CommandWidgetElement(toolbarSectionElement.Items)
            {
                Name = "SaveChangesWidgetElement",
                ButtonType = CommandButtonType.Save,
                CommandName = DefinitionsHelper.SaveCommandName,
                Text = (isCreateMode) ? String.Concat("Create Location") : "Save Changes",
                WrapperTagKey = HtmlTextWriterTag.Span,
                WidgetType = typeof(CommandWidget)
            });

            // Preview
            if (showPreview == true)
            {
                toolbarSectionElement.Items.Add(new CommandWidgetElement(toolbarSectionElement.Items)
                {
                    Name = "PreviewWidgetElement",
                    ButtonType = CommandButtonType.Standard,
                    CommandName = DefinitionsHelper.PreviewCommandName,
                    Text = "Preview",
                    ResourceClassId = typeof(Labels).Name,
                    WrapperTagKey = HtmlTextWriterTag.Span,
                    WidgetType = typeof(CommandWidget)
                });
            }

            // show Actions menu
            if (!isCreateMode)
            {
                var actionsMenuWidget = new ActionMenuWidgetElement(toolbarSectionElement.Items)
                {
                    Name = "moreActions",
                    Text = Res.Get<Labels>().MoreActionsLink,
                    WrapperTagKey = HtmlTextWriterTag.Div,
                    WidgetType = typeof(ActionMenuWidget),
                    CssClass = "sfInlineBlock sfAlignMiddle"
                };
                actionsMenuWidget.MenuItems.Add(new CommandWidgetElement(actionsMenuWidget.MenuItems)
                {
                    Name = "DeleteCommand",
                    Text = "DeleteThisItem",
                    CommandName = DefinitionsHelper.DeleteCommandName,
                    WidgetType = typeof(CommandWidget),
                    CssClass = "sfDeleteItm"
                });

                toolbarSectionElement.Items.Add(actionsMenuWidget);
            }

            // Cancel button
            toolbarSectionElement.Items.Add(new CommandWidgetElement(toolbarSectionElement.Items)
            {
                Name = "CancelWidgetElement",
                ButtonType = CommandButtonType.Cancel,
                CommandName = DefinitionsHelper.CancelCommandName,
                Text = "Back to Locations List",
                WrapperTagKey = HtmlTextWriterTag.Span,
                WidgetType = typeof(CommandWidget)
            });

            detailView.Toolbar.Sections.Add(toolbarSectionElement);
        }

        #endregion

Backend Sections Helper Methods

Another helper method CreateBackendSections is used to define the actual form for editing, creating, or previewing items. This is where you specify all of the controls used to set the various properties and content for your custom content item.

Sitefinity-4-Locations-Module-Editor

Once again the full code for this method is lengthy, but will vary depending on how many fields your module's data item contains.

The outline for the method is below, and the complete, expanded code snippet can be seen here or downloaded in the project available at the end of this post.

 #region Backend Section Forms

        /// <summary>
        /// Creates the backend sections. Adds edit/preview controls to the detailView
        /// </summary>
        /// <param name="detailView">The detail view.</param>
        /// <param name="displayMode">The display mode.</param>
        private static void CreateBackendSections(DetailFormViewElement detailView, FieldDisplayMode displayMode)
        {
            // define main content section
            var mainSection = new ContentViewSectionElement(detailView.Sections)
            {
                Name = "MainSection",
                CssClass = "sfFirstForm"
            };

            #region Title Field

            #region Content

            #region Address fields

            #region Categories and Tags Section

            #region More options Section
 }

        #endregion

The form is made up of a collection of ContentViewSectionElement items. Each of these represents a separate section of the form, such as the item properties, taxonomy, Urls, etc. You can add as many of these as is needed by your module.

Each section then has a collection of elements used for viewing or editing (depending on the mode of the form) the item data. For example, the "Address" property uses a TextFieldDefinitionElements, while the "Content" property is edited using the HtmlFieldElement.

There are many different types of elements you can use, and as you'll see in the complete code snippet, support several properties as well as validation.

Each of the #regions in this method simply create the elements necessary to support editing (or previewing) these properties. The More Options Section is used to allow users to edit the Url for the content item.

Sitefinity-4-Locations-Module-Editorp-More-Options

Configuration

now that we have completed the definition of our Backend, add it to your module by modifying the InitializeDefaultViews method in the LocationsConfig class.

/// <summary>
/// Initializes the default backend and frontend views.
/// </summary>
/// <param name="contentViewControls"></param>
protected override void InitializeDefaultViews(ConfigElementDictionary<string, ContentViewControlElement> contentViewControls)
{
    // add backend views to configuration
    contentViewControls.Add(LocationsDefinitions.DefineLocationsBackendContentView(contentViewControls));
}

What's Next

The backend is now completed and in our next post we'll look at creating the Frontend controls, including loading embedded resources with the new Virtual Path Provider.

The completed project up to this point is available for download below. Be sure to send any questions to the Sitefinity 4 SDK Forum and share your experiences with our community.

Downloads

    progress-logo

    The Progress Team

    View all posts from The Progress Team on the Progress blog. Connect with us about all things application development and deployment, data integration and digital business.

    Comments

    Comments are disabled in preview mode.
    Topics

    Sitefinity Training and Certification Now Available.

    Let our experts teach you how to use Sitefinity's best-in-class features to deliver compelling digital experiences.

    Learn More
    Latest Stories
    in Your Inbox

    Subscribe to get all the news, info and tutorials you need to build better business apps and sites

    Loading animation