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.