Select the search type
  • Site
  • Web
Search
You are here:  Company/News

Blog2mind

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.

Archive