Products

Solutions

Resources

Partners

Community

About

New Community Website

Ordinarily, you'd be at the right spot, but we've recently launched a brand new community website... For the community, by the community.

Yay... Take Me to the Community!

The Community Blog is a personal opinion of community members and by no means the official standpoint of DNN Corp or DNN Platform. This is a place to express personal thoughts about DNNPlatform, the community and its ecosystem. Do you have useful information that you would like to share with the DNN Community in a featured article or blog? If so, please contact .

The use of the Community Blog is covered by our Community Blog Guidelines - please read before commenting or posting.


Strong typing your settings

This is something of a pet peeve for me. And whenever I get myself involved in an open source project in DNN, it is one of the first things I’d tackle. The issue I’m referring to, here, is how to properly code settings that are serialized to storage (SQL). So settings are commonly retrieved as a hashtable or dictionary of key/value pairs that are both of a string type. These lists get stored in SQL typically in tables with the “Settings” suffix. I.e. ModuleSettings, PortalSettings, TabModuleSettings, etc. And often modules implement their own settings table because the framework hard wires ModuleSettings to … modules. So if you need portal-scoped settings for your module, you’ll need to create your own table. The old blog module (pre version 6) used to do this, for instance.

So, to begin with the worst of scenarios, let’s look at the version 3 code of the blog module. Here is a snippet from BlogList.ascx.vb:

    BlogSettings = Utility.GetBlogModuleSettings(Me.PortalId, TabId)
    If CType(BlogSettings("PageBlogs"), String) <> "" Then
     m_PersonalBlogID = CType(BlogSettings("PageBlogs"), Integer)
    Else
     m_PersonalBlogID = -1
    End If

The BlogSettings here is a hashtable that has been read from the database. It’s easy to see what is happening, here. First we retrieve the settings hashtable. Then we check if the value for the key “PageBlogs” is an empty string. Note that here is already a first mistake as this will throw an error if the key is not present. Then, if there is a string value there, it is parsed as an integer. Again, this would bomb if the string was not an integer, but this I’ll waive as only the module is setting this value. But it’s not pretty. Finally, if no value was found in the string the value is initialized to –1.

Now you’d look at this and go “yikes”, right? I hope so. Not only do we have several points of failure, we are also managing the settings very close to the UI layer. I.e. we are working to convert from and to settings values right in the codebehind of the ascx, whereas this should ideally be concentrated in the business layer. Not convinced? What if I told you this code reappears in Archive.ascx.vb, Blog.ascx.vb, ModuleOptions.ascx.vb, Search.ascx.vb and ViewBlog.ascx.vb. Messy enough for you? Indeed it’s unbelievable.

Strong typing means we create a wrapper around all this junk and all other code can refer to Settings.PageBlogs which happens to be an integer. So the first step is to create a separate class to hold your settings. For this example I’ll assume we’re doing a “simple” module which stores its settings in ModuleSettings. As you may know these module settings are actually propagated to the UI layer through the Settings property on the PortalModuleBase class that most of your controls inherit from. Again, this is a hashtable. And our goal will be to wrap this up as neatly as we possibly can so we don’t leak any settings management to other parts of our code.

Step 1: Create your settings class

Namespace Common
 Public Class ModuleSettings

#Region " Properties "
  Private Property ModuleId As Integer = -1
  Private Property Settings As Hashtable
  Public Property ShowProjects As Boolean = True
#End Region

What I’ve done is to add 3 properties to this class. The first two are internal properties that I’ll use to manage the settings. The latter (ShowProjects) is my first public setting that I need in my code. Note I’m already initializing the value of that setting. This is important as we’ll see later on.

Step 2: Create the constructor to deserialize the hashtable

  Public Sub New(ByVal ModuleId As Integer)
   _ModuleId = ModuleId
   _Settings = (New DotNetNuke.Entities.Modules.ModuleController).GetModuleSettings(ModuleId)
   ShowProjects = _Settings.GetValue(Of Boolean)("ShowProjects", ShowProjects)
  End Sub

Here we store the module id (which we need later if we want to save settings again) and we retrieve the hashtable, first. Then we try to get the value out of the hashtable to our setting. Note DNN now includes an extension method to simplify this process called GetValue. It does all of what the code did we saw earlier in the example from the blog module, but more efficiently. We now have one line and it will not throw an error if the value is not there and just use the default value if that is so (this is why we need to initialize our properties when we declare them). In fact, I’ve been nagging the core architects to have these methods available for us all so I didn’t need to code similar logic myself all the time.

Step 3: Add caching

For this we’ll use a static constructor as follows:

  Public Shared Function GetModuleSettings(ByVal ModuleId As Integer) As ModuleSettings
   Dim modSettings As ModuleSettings = Nothing
   Try
    modSettings = CType(DotNetNuke.Common.Utilities.DataCache.GetCache(CacheKey(ModuleId)), ModuleSettings)
   Catch
   End Try
   If modSettings Is Nothing Then
    modSettings = New ModuleSettings(ModuleId)
    DotNetNuke.Common.Utilities.DataCache.SetCache(CacheKey(ModuleId), modSettings)
   End If
   Return modSettings
  End Function

  Private Shared Function CacheKey(ByVal ModuleId As Integer) As String
   Return "ModuleSettings" & ModuleId.ToString
  End Function

Here we see how we can make sure we try to cache the complete settings object in DNN’s cache. Note, we let DNN take care of the caching time/expiration/etc.

Step 4: Saving the settings

  Public Sub Save()
   Dim objModules As New DotNetNuke.Entities.Modules.ModuleController
   objModules.UpdateModuleSetting(_ModuleId, "ShowProjects", Me.ShowProjects.ToString)
   DotNetNuke.Common.Utilities.DataCache.SetCache(CacheKey(_ModuleId), Me)
  End Sub

Saving becomes trivially easy as we have all the necessary bits and pieces in place. Note we are no longer doing the settings management for this in the codebehind of the edit control that is shown to the user when editing the settings. All is done within the settings class.

Step 5: Add to your base class

Of course you are using your own class that inherits from PortalModuleBase, right? No? Then you should. And here’s why:

  Private _settings As ModuleSettings
  Public Shadows Property Settings As ModuleSettings
   Get
    If _settings Is Nothing Then
     _settings = ModuleSettings.GetModuleSettings(ModuleId)
    End If
    Return _settings
   End Get
   Set(value As ModuleSettings)
    _settings = value
   End Set
  End Property

What happens here is that the Settings in the good old PortalModuleBase are now being replaced by our own settings class. So now, in your UI code you’ll just use Settings.ShowProjects to access our setting.

Shouldn’t we also do this in the core?

You can probably guess my answer. Yes, we should. You didn’t know that the core is still rife with the code I’ve been lamenting above? Check out the latest source version (7.3.1 as of this writing) and head over to UserController.cs lines 256 and below. You’ll notice blocks like these:

            if (settings["Profile_DisplayVisibility"] == null)
            {
                settings["Profile_DisplayVisibility"] = true;
            }

Here the settings dictionary is being primed to catch errors in case no value is there. Oh dear. That means that the conversions are done at the end point in code. And if we search for the specific string we’ll find it in lines 59-66 of Profile.ascx.cs:

        protected bool ShowVisibility
        {
            get
            {
                object setting = GetSetting(PortalId, "Profile_DisplayVisibility");
                return Convert.ToBoolean(setting) && IsUser;
            }
        }

As we can see we have the string Profile_DisplayVisibility appearing three times. And if they need this setting more often, this string would keep on appearing. The repetition of this string has two major drawbacks: (1) it increases the likelihood of a coding errors by those working on the core code (if you’d misspell Profile_DisplayVisibility you won’t get the setting) and (2) it makes it more difficult for people like myself that work just with the API of the core to determine which settings there are, what type they are and what they’re called. This begs for refactoring. And I sincerely hope this post will make it clear what it is I’m after. I’ve gone ahead and coded a start for this bit of the framework. So if you examine the UserSettings they consist of a set of “sections” (Profile, Security, etc). This can easily be done in a very elegant way as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using DotNetNuke.Common;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Controllers;
using DotNetNuke.Entities.Portals;

namespace DotNetNuke.Entities.Users
{
    class UserSettings
    {
        public const string CacheKey = "UserSettingsPortal{0}";

        #region Public Properties
        private int PortalId { get; set; }
        private string CultureCode { get; set; }
        public ProfileSettings Profile { get; set; }

        #endregion

        public static UserSettings GetUserSettings(int portalId, string cultureCode)
        {
            string cacheKey = string.Format(CacheKey, portalId);
            return CBO.GetCachedObject<UserSettings>(new CacheItemArgs(cacheKey, DataCache.PortalSettingsCacheTimeOut, DataCache.PortalSettingsCachePriority, portalId, cultureCode), GetUserSettingsCallback, true);
        }
        private static object GetUserSettingsCallback(CacheItemArgs cacheItemArgs)
        {
            var portalId = (int)cacheItemArgs.ParamList[0];
            var cultureCode = (string)cacheItemArgs.ParamList[1];
            Dictionary<string, string> settingsDictionary = (portalId == Null.NullInteger)
                                                ? HostController.Instance.GetSettingsDictionary()
                                                : PortalController.GetPortalSettingsDictionary(PortalController.GetEffectivePortalId(portalId));
            UserSettings res = new UserSettings();
            res.Profile = new ProfileSettings(settingsDictionary);
            return res;
        }

        public void Save()
        {
            PortalController.UpdatePortalSetting(PortalId, "Profile_DefaultVisibility", Profile.DefaultVisibility.ToString(), false, CultureCode);
            PortalController.UpdatePortalSetting(PortalId, "Profile_DisplayVisibility", Profile.DisplayVisibility.ToString(), true, CultureCode);
        }


        class ProfileSettings
        {
            #region Public Properties
            public int DefaultVisibility { get; set; }
            public bool DisplayVisibility { get; set; }
            #endregion

            public ProfileSettings(Dictionary<string, string> settingsDictionary)
            {
                DefaultVisibility = settingsDictionary.GetValue<int>("Profile_DefaultVisibility", DefaultVisibility);
                DisplayVisibility = settingsDictionary.GetValue<bool>("Profile_DisplayVisibility", DisplayVisibility);
            }
        }

    }
}

This implements the four first steps listed above and as a bonus stacks the settings so you could use UserSettings.GetUserSettings(portalId, cultureCode).Profile.DefaultVisiblity. Now that would be a lot clearer. And here’s another good reason to take the time to do this: we can document these properties. Making the framework even more insanely easy to work with.

Comments

Peter Schotman
Thanks, helpful, for the C# readers: https://dotnetfiddle.net/0ajZ3R
Peter Schotman Tuesday, July 1, 2014 3:32 AM (link)
Horacio Judeikin
I thought host, portal, tabs and module's setting were automagically being cached. I looks like at least for module's settings this is not true.
Horacio Judeikin Tuesday, July 1, 2014 10:50 AM (link)
Jay Mathis
Excellent post Peter

I'd like to suggest another method I've started using recently that I think is even easier, which is storing the settings class as a JSON object in the database.

The great thing about this method is the built-in JSON utilities will automatically match up the field names from the JSON object to the properties of the class. And if you later add or delete properties, it is all handled for you automatically.

Something like this (taken from a custom slider/rotator module - shortened for brevity)

First a class to hold all the settings options:
[Serializable]
public class Options
{
public int delay { get; set; }
public int startwidth { get; set; }
public int startheight { get; set; }:

// initialize the class with some defaults
public Options()
{
delay = 9000;
startwidth = 960;
startheight = 350;
}
}

Next, to set it and save the options:
Options slider_options = new Options();
ModuleController mc = new ModuleController();

//layout
mc.UpdateTabModuleSetting(TabModuleId, "layout", ddlSliderLayout.SelectedValue);

// full screen offset container
slider_options.fullScreenOffsetContainer = txtFullscreenOffsetContainer.Text;

//width
slider_options.startwidth = int.Parse(txtWidth.Text);

//height
slider_options.startheight = int.Parse(txtHeight.Text);

//delay
slider_options.delay = int.Parse(txtDelay.Text);


Jay Mathis Tuesday, July 1, 2014 3:29 PM (link)
Jay Mathis
Oops, posted too soon... here it is:

First a class to hold all the settings options:
[Serializable]
public class Options
{
public int delay { get; set; }
public int startwidth { get; set; }
public int startheight { get; set; }:

// initialize the class with some defaults
public Options()
{
delay = 9000;
startwidth = 960;
startheight = 350;
}
}

Next, to set it and save the options:
Options slider_options = new Options();
ModuleController mc = new ModuleController();

//delay
slider_options.delay = int.Parse(txtDelay.Text);

//width
slider_options.startwidth = int.Parse(txtWidth.Text);

//height
slider_options.startheight = int.Parse(txtHeight.Text);

mc.UpdateTabModuleSetting(TabModuleId, "options", slider_options.ToJson());


And to get the values out:

Options slider_options = new Options();

//options
if (Settings.Contains("options"))
{
string _slider_options = Settings["options"].ToString();
slider_options = _slider_options.FromJson();
}

//delay
txtDelay.Text = slider_options.delay.ToString();

//width
txtWidth.Text = slider_options.startwidth.ToString();

//height
txtHeight.Text = slider_options.startheight.ToString();

Jay Mathis Tuesday, July 1, 2014 3:32 PM (link)
Jay Mathis
hmm, the editor here is stripping out part of the FromJson() call because it looks like HTML

Let me try this:
slider_options = _slider_options.FromJson < Options > ();
Jay Mathis Tuesday, July 1, 2014 3:36 PM (link)
Horacio Judeikin
Back to the caching issue, I see that settings are indeed being cached by the DNN Core.
For example, ModuleController.GetModuleSettings() the first thing it does is:
string cacheKey = String.Format(DataCache.ModuleSettingsCacheKey, tabId);

var moduleSettings = CBO.GetCachedObject>(new CacheItemArgs(cacheKey,
DataCache.ModuleCacheTimeOut,
DataCache.ModuleCachePriority),

Is your caching code redundant, or am I missing something?
Horacio Judeikin Sunday, July 20, 2014 9:42 AM (link)

Comment Form

Only registered users may post comments.

NewsArchives


Aderson Oliveira (22)
Alec Whittington (11)
Alessandra Daniels (3)
Alex Shirley (10)
Andrew Hoefling (3)
Andrew Nurse (30)
Andy Tryba (1)
Anthony Glenwright (5)
Antonio Chagoury (28)
Ash Prasad (37)
Ben Schmidt (1)
Benjamin Hermann (25)
Benoit Sarton (9)
Beth Firebaugh (12)
Bill Walker (36)
Bob Kruger (5)
Bogdan Litescu (1)
Brian Dukes (2)
Brice Snow (1)
Bruce Chapman (20)
Bryan Andrews (1)
cathal connolly (55)
Charles Nurse (163)
Chris Hammond (213)
Chris Paterra (55)
Clint Patterson (108)
Cuong Dang (21)
Daniel Bartholomew (2)
Daniel Mettler (181)
Daniel Valadas (48)
Dave Buckner (2)
David Poindexter (12)
David Rodriguez (3)
Dennis Shiao (1)
Doug Howell (11)
Erik van Ballegoij (30)
Ernst Peter Tamminga (80)
Francisco Perez Andres (17)
Geoff Barlow (12)
George Alatrash (12)
Gifford Watkins (3)
Gilles Le Pigocher (3)
Ian Robinson (7)
Israel Martinez (17)
Jan Blomquist (2)
Jan Jonas (3)
Jaspreet Bhatia (1)
Jenni Merrifield (6)
Joe Brinkman (274)
John Mitchell (1)
Jon Henning (14)
Jonathan Sheely (4)
Jordan Coopersmith (1)
Joseph Craig (2)
Kan Ma (1)
Keivan Beigi (3)
Kelly Ford (4)
Ken Grierson (10)
Kevin Schreiner (6)
Leigh Pointer (31)
Lorraine Young (60)
Malik Khan (1)
Matt Rutledge (2)
Matthias Schlomann (16)
Mauricio Márquez (5)
Michael Doxsey (7)
Michael Tobisch (3)
Michael Washington (202)
Miguel Gatmaytan (3)
Mike Horton (19)
Mitchel Sellers (40)
Nathan Rover (3)
Navin V Nagiah (14)
Néstor Sánchez (31)
Nik Kalyani (14)
Oliver Hine (1)
Patricio F. Salinas (1)
Patrick Ryan (1)
Peter Donker (54)
Philip Beadle (135)
Philipp Becker (4)
Richard Dumas (22)
Robert J Collins (5)
Roger Selwyn (8)
Ruben Lopez (1)
Ryan Martinez (1)
Sacha Trauwaen (1)
Salar Golestanian (4)
Sanjay Mehrotra (9)
Scott McCulloch (1)
Scott Schlesier (11)
Scott Wilkinson (3)
Scott Willhite (97)
Sebastian Leupold (80)
Shaun Walker (237)
Shawn Mehaffie (17)
Stefan Cullmann (12)
Stefan Kamphuis (12)
Steve Fabian (31)
Steven Fisher (1)
Tony Henrich (3)
Torsten Weggen (3)
Tycho de Waard (4)
Vicenç Masanas (27)
Vincent Nguyen (3)
Vitaly Kozadayev (6)
Will Morgenweck (40)
Will Strohl (180)
William Severance (5)
What is Liquid Content?
Find Out
What is Liquid Content?
Find Out
What is Liquid Content?
Find Out