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

Creating Custom Configuration Settings in Sitefinity 4

by Josh Morales

When extending Sitefinity via a custom Intra-Site Module, you'll often encounter the need to store and retrieve settings, either for user preferences or any other process within your module. There are many ways to do this yourself, including a custom table, flat text and xml files.

Fortunately, Sitefinity has a powerful configurations manager that you can tap into quite easily to store and manage all kinds of information for your custom modules. Best of all, because this is a native part of Sitefinity, you also inherit the ability to manage these settings within the existing Sitefinity Advanced Settings Manager.

As we saw in the post about the new Sitemap Module SDK Sample, tapping into this feature is as simple as implementing a few classes. Today we'll take a closer look at the configuration classes of the Sitemap Module and show you how easy it is to store custom settings.

ConfigSection

The first thing we need is a class that inherits from ConfigSection. This defines the overall configuration class for the module, and corresponds to the associated .config file that will be created in ~/App_Data/Configuration.

ConfigSection Properties

Adding properties to this class defines the configuration settings that are persisted. In order for this to work, each attribute needs to be include an attribute describing both the name of the property (using ConfigurationProperty) and the type of the property. There are many types available for your configuration element collections, including Dictionary and List.

In this case, we are defining a property of type ConfigElementDictionary named ContentTypes, which will refer to any of the content modules (Blogs, News, etc.). The implementation maps our property to the base ConfigElement using a string as the key. Basically what we have done is create a strongly-typed wrapper for storing our configuration element.

/// <summary>/// Configuration class for the SiteMap Module/// </summary>public class SiteMapConfig : ConfigSection{
    /// <summary> /// The collection of Sitefinity Content Types to be indexed and their associated settings. /// </summary> [ConfigurationProperty("ContentTypes"), ConfigurationCollection(typeof(ContentType), AddItemName = "ContentType")]
    public ConfigElementDictionary<string, ContentType> ContentTypes
    {
        get { return (ConfigElementDictionary<string, ContentType>)this["ContentTypes"]; }
    }
}

The value portion of this dictionary ContentType is another custom class we will define that inherits from ConfigElement. The reason we’ve done this is that the data we are storing for each module has its own multiple, custom properties and can’t be stored as a simple string.

If you were storing a string, you could easily replace the Dictionary type with something like ConfigElementDictionary<string, string> and be done. However, because in our example, each module can have multiple providers, we need the ability to store multiple pages of settings.

So that’s why we define the following ContentType class to store them. This time we are using a different configuration element (ConfigElementList), allowing us to support storing multiple items for this property.

/// <summary>/// Configuration Element for storing list pages for Sitefinity Content Types/// </summary>public class ContentType : ConfigElement{
    /// <summary> /// Initializes a new instance of the <see cref="ContentType"/> class. /// </summary> /// <param name="parent">The parent.</param> public ContentType(ConfigElement parent) : base(parent) { this.Pages = new ConfigElementList<ContentPage>(this); }

    /// <summary> /// Gets or sets the name of the ContentType. This is the same as the Module Name of the Content Type. /// </summary> /// <value> /// The name of the Content Module. /// </value> [ConfigurationProperty("Name", Options = ConfigurationPropertyOptions.IsKey | ConfigurationPropertyOptions.IsRequired, DefaultValue = "")]
    public string Name
    {
        get { return (string)base["Name"]; }
        set { base["Name"] = value; }
    }

    /// <summary> /// List of ContentPage configuration elements containing the default list page for each provider of this ContentType /// </summary> /// <value> /// The list of default list pages, one per ContentType provider. /// </value> [ConfigurationProperty("Pages", IsRequired = true)]
    public ConfigElementList<ContentPage> Pages
    {
        get { return (ConfigElementList<ContentPage>)base["Pages"]; }
        set { base["Pages"] = value; }
    }
}

Once again, we are using yet another custom class, ContentPage, which again inherits from ConfigElement. Like the ContentType class above, this is needed because for each page we need to persist multiple properties, including Name, Provider, and the DefaultUrl.

Fortunately, this is the last complex type, and we can define this class in terms of these simple properties.

/// <summary>/// Configuration Element for storing the default list page for a Sitefinity Content Type/// </summary>public class ContentPage : ConfigElement{
    /// <summary> /// Initializes a new instance of the <see cref="ContentPage"/> class. /// </summary> /// <param name="parent">The parent.</param> public ContentPage(ConfigElement parent) : base(parent) { }

    /// <summary> /// Initializes a new instance of the <see cref="ContentPage"/> class. /// </summary> /// <param name="parent">The parent config element.</param> /// <param name="ProviderName">Name of the Sitefinity Content Type Provider.</param> /// <param name="PageID">The Guid of the Sitefinity Page ID.</param> public ContentPage(ConfigElement parent, string ProviderName, string PageID) : base(parent)
    {
        this.ProviderName = ProviderName;
        this.DefaultPageUrl = PageID;
    }

    /// <summary> /// For flat content types (everything except blogs) this is the same as the ProviderName. /// For Blogs, this is the Url-Name for the blog. /// </summary> /// <value> /// The Content Provider name (or Url-Name for a blog). /// </value> [ConfigurationProperty("Name", Options = ConfigurationPropertyOptions.IsKey | ConfigurationPropertyOptions.IsRequired, DefaultValue = "")]
    public string Name
    {
        get { return (string)base["Name"]; }
        set { base["Name"] = value; }
    }

    /// <summary> /// Gets or sets the name of the Sitefinity content type provider. /// </summary> /// <value> /// The provider name for the Sitefinity content type /// </value> [ConfigurationProperty("ProviderName", Options = ConfigurationPropertyOptions.IsRequired, DefaultValue = "")]
    public string ProviderName
    {
        get { return (string)base["ProviderName"]; }
        set { base["ProviderName"] = value; }
    }

    /// <summary> /// Gets or sets the default page URL for this content type and provider. /// </summary> /// <value> /// The default page URL for this content type and provider. /// </value> [ConfigurationProperty("DefaultPageUrl", Options = ConfigurationPropertyOptions.IsRequired, DefaultValue = "")]
    public string DefaultPageUrl
    {
        get { return (string)base["DefaultPageUrl"]; }
        set { base["DefaultPageUrl"] = value; }
    }

    /// <summary> /// Gets or sets a value indicating whether this <see cref="ContentPage"/> should be included in the Sitemap. /// </summary> /// <value> /// <c>true</c> if this content type should be included in the Sitemap; otherwise, <c>false</c>. /// </value> [ConfigurationProperty("Include", Options = ConfigurationPropertyOptions.IsRequired, DefaultValue = true)]
    public bool Include
    {
        get { return (bool)base["Include"]; }
        set { base["Include"] = value; }
    }
}

Configuration Files and Default Properties

At this point, our configuration class is complete and ready to use! However, before we install it into our module, we want to add some convenience for our users by attempting to automatically set some default values. This way, the user only has to modify the configuration if our guess for the default pages is incorrect.

To do this, you simply need to override the OnPropertiesInitialized method of the main SiteMapConfig class. This is called when the configuration is initialized, and is used to populate the configuration with default values.

One important thing to note about this: the default values are always persisted, but only within the configuration class itself (in other words, in memory while the website runs). If the default values discovered and set by the OnPropertiesInitialized method are all correct and therefore not modified, the associated siteMapConfig.config file will be blank.

This is how all configurations in Sitefinity work; the config files are only updated and written to if the values of the configuration property are different from their default value.

So let's go ahead and define the method that will scan through the website and attempt to locate the default pages, creating the default values for our configuration. For more information on how this algorithm works, be sure to look at the previous post: New Sitefinity 4.4 SDK Sample: Sitemap Module.

/// <summary>/// Called after the properties of this instance have been initialized./// Load default values here./// </summary>protected override void OnPropertiesInitialized()
{
    base.OnPropertiesInitialized();

    #region News

    // ensure news config exists if (!this.ContentTypes.ContainsKey(NewsModule.ModuleName))
    {
        var contentType = new ContentType(this.ContentTypes);
        contentType.Name = NewsModule.ModuleName;
        contentType.Pages = new ConfigElementList<ContentPage>(contentType);
        this.ContentTypes.Add(contentType);
    }

    // ensure news pages config exist var newsConfig = this.ContentTypes[NewsModule.ModuleName];
    if (newsConfig.Pages.Count == 0)
    {
        // add one setting for each provider var providers = NewsManager.GetManager().Providers;
        foreach (var provider in providers)
        {
            // retrieve all pages with a NewsView on them var NewsPages = App.WorkWith().Pages()
                .Where(p => p.Page != null && p.ShowInNavigation && p.Page.Controls.Where(c => c.ObjectType.StartsWith(typeof(NewsView).FullName)).Count() > 0)
                .Get();

            // attempt to locate the default page foreach (var page in NewsPages)
            {
                string providerName;

                // retrieve the news view control from the page var newsView = page.Page.Controls.Where(c => c.ObjectType.StartsWith(typeof(NewsView).FullName)).First();
                if (newsView == null) continue;

                // determine if there is a modified control definition var controlDefinition = newsView.Properties.Where(p => p.Name == "ControlDefinition").FirstOrDefault();
                if (controlDefinition == null)
                    // control uses default provider providerName = NewsManager.GetDefaultProviderName();
                else {
                    // search for modified provider name in the control var providerNameProperty = controlDefinition.ChildProperties.Where(p => p.Name == "ProviderName").FirstOrDefault();
                    if (providerNameProperty == null)
                        // control is modified, but still uses default provider providerName = NewsManager.GetDefaultProviderName();
                    else // get selected provider name providerName = providerNameProperty.Value;
                }

                // make sure specified provider matches the current provider we are working with if (providerName != provider.Name) continue;

                // make sure the mode of the control is not Details-only, skip this page if it is var displayModeProperty = newsView.Properties.Where(p => p.Name == "ContentViewDisplayMode").SingleOrDefault();
                if (displayModeProperty != null && displayModeProperty.Value == Telerik.Sitefinity.Web.UI.ContentUI.Enums.ContentViewDisplayMode.Detail.ToString()) continue;

                // save default news page for this provider to the config var newsPage = new ContentPage(newsConfig.Pages);
                newsPage.DefaultPageUrl = page.GetFullUrl();
                newsPage.Name = providerName;
                newsPage.Include = true;
                newsPage.ProviderName = providerName;
                newsConfig.Pages.Add(newsPage);

                // stop search for a matching news page, move to next provider (if any) break;
            }
        }
    }

    #endregion #region Events

    // ensure events config exists if (!this.ContentTypes.ContainsKey(EventsModule.ModuleName))
    {
        var contentType = new ContentType(this.ContentTypes);
        contentType.Name = EventsModule.ModuleName;
        contentType.Pages = new ConfigElementList<ContentPage>(contentType);
        this.ContentTypes.Add(contentType);
    }

    // ensure events pages config exists var eventsConfig = this.ContentTypes[EventsModule.ModuleName];
    if (eventsConfig.Pages.Count == 0)
    {
        // add one setting for each provider var providers = EventsManager.GetManager().Providers;
        foreach (var provider in providers)
        {
            // retrieve all pages that contain an EventsView var eventsPages = App.WorkWith().Pages()
                .Where(p => p.Page != null && p.ShowInNavigation && p.Page.Controls.Where(c => c.ObjectType.StartsWith(typeof(EventsView).FullName)).Count() > 0)
                .Get();

            // attempt to locate the default page foreach (var page in eventsPages)
            {
                string providerName;

                // retrieve the events view control var eventsView = page.Page.Controls.Where(c => c.ObjectType.StartsWith(typeof(EventsView).FullName)).First();
                if (eventsView == null) continue;

                // determine if there is a modified control definition var controlDefinition = eventsView.Properties.Where(p => p.Name == "ControlDefinition").FirstOrDefault();
                if (controlDefinition == null)
                    // control uses default provider providerName = EventsManager.GetDefaultProviderName();
                else {
                    // search for modified provider name var providerNameProperty = controlDefinition.ChildProperties.Where(p => p.Name == "ProviderName").FirstOrDefault();
                    if (providerNameProperty == null)
                        // control is modified but still uses default provider providerName = EventsManager.GetDefaultProviderName();
                    else // get custom provider name providerName = providerNameProperty.Value;
                }

                // make sure specified provider matches the current provider if (providerName != provider.Name) continue;

                // skip page if it is a details-mode page var displayModeProperty = eventsView.Properties.Where(p => p.Name == "ContentViewDisplayMode").SingleOrDefault();
                if (displayModeProperty != null && displayModeProperty.Value == Telerik.Sitefinity.Web.UI.ContentUI.Enums.ContentViewDisplayMode.Detail.ToString()) continue;

                // save page to config var eventsPage = new ContentPage(eventsConfig.Pages);
                eventsPage.DefaultPageUrl = page.GetFullUrl();
                eventsPage.Name = providerName;
                eventsPage.Include = true;
                eventsPage.ProviderName = providerName;
                eventsConfig.Pages.Add(eventsPage);

                // stop search for default page, move to next provider (if any) break;
            }
        }
    }

    #endregion #region Blogs

    // ensure blog config exists if (!this.ContentTypes.ContainsKey(BlogsModule.ModuleName))
    {
        var contentType = new ContentType(this.ContentTypes);
        contentType.Name = BlogsModule.ModuleName;
        contentType.Pages = new ConfigElementList<ContentPage>(contentType);
        this.ContentTypes.Add(contentType);
    }

    // ensure blogs pages config exist var blogsConfig = this.ContentTypes[BlogsModule.ModuleName];
    if (blogsConfig.Pages.Count == 0)
    {
        // add a config setting for each provider var providers = BlogsManager.GetManager().Providers;
        foreach (var provider in providers)
        {
            // retrieve all in the provider var blogs = App.Prepare().SetContentProvider(provider.Name).WorkWith().Blogs().Get();

            // add a config setting for each blog foreach (var blog in blogs)
            {
                // make sure default page is set if (!blog.DefaultPageId.HasValue || blog.DefaultPageId.Value == Guid.Empty) continue;

                // get default page url var defaultPage = App.WorkWith().Page(blog.DefaultPageId.Value).Get();
                if (defaultPage == null) continue;

                // save default blog page to config var blogPage = new ContentPage(blogsConfig.Pages);
                blogPage.DefaultPageUrl = defaultPage.GetFullUrl();
                blogPage.Name = blog.UrlName;
                blogPage.Include = true;
                blogPage.ProviderName = provider.Name;
                blogsConfig.Pages.Add(blogPage);
            }
        }
    }

    #endregion}

Installing Configuration

Now that everything is in place, we only need to register the configuration for use by our module. This is done in two places. First we need to override the GetModuleConfig method and simply return our custom configuration:

/// <summary>/// Gets the module config./// </summary>/// <returns></returns>protected override ConfigSection GetModuleConfig()
{
    return Config.Get<SiteMapConfig>();
}

Finally, in the module Install method, we need to register our configuration section:

/// <summary>/// Initializes the service with specified settings./// </summary>/// <param name="settings">The settings.</param>public override void Initialize(ModuleSettings settings)
{
    base.Initialize(settings);
    Config.RegisterSection<SiteMapConfig>();

    // handle the sitemap path RouteTable.Routes.Add("SiteMap", new Route("sitemap.xml", new SiteMapRouteHandler()));
}

Not only does this allow us to use our new configuration classes in code, but it is also automatically registered and accessible within the main Sitefinity Administration Settings!

Sitefinity-4-Custom-Configuration-Settings

Wrapping Up

By inheriting from a few simple classes, we are able to quickly define a full-featured configuration class, with default values, a backing xml file, and even support for editing configuration settings from right inside Sitefinity itself. This process will allow you to easily persist your own settings within your custom modules and controls.

Try it out for yourself, and be sure to download the Sitefinity SDK and try out the SiteMap Module for yourself. As always, be sure to share your experiences, questions, and comments with us in the Sitefinity SDK Forum.

2 comments

Leave a comment
  1. Bert Jun 10, 2013
    Hi Josh,

    This is a great guide on how to set this up. Can it be adjusted so that these setting appear under the "Basic settings" instead of the "Advanced settings?"
  2. Emanuele Apr 18, 2014
    Yeah, how to add in Basic Settings?

    Leave a comment