Hi there again. Today’s topic for some deep exploration is how to manage your module’s settings in code. As I stated in my previous posts (here and here), most examples of DotNetNuke module development out in the wild are fine for a first shot at a module, but won’t hack it when you begin to get serious with module development. And if you don’t believe me: come to a DNN event and ask any one of the more experienced DNN Store vendors. It’s not that the Hello World examples suck. It’s just not sustainable when you’re building your module for world domination.
So once again: hi, my name is Peter Donker. I run a small business called Bring2mind which specializes in document management on the DotNetNuke platform.
Me with my DNN wizard hat on.
Module settings at the “Hello World” level
Even the most trivial of modules will have some settings that the admin/editor can change to control behaviour. Commonly we store these in a “property bag” called Settings which is a property of the base class of our module’s code (PortalModuleBase). This property bag comes in the form of a hashtable. So a list of object/object pairs. The values of these stored in a table called ModuleSettings … as strings. So the objects will get serialized to strings. What does that mean? Well, if you add a setting as follows:
Me.Settings.Add(“Foo”, bar)
Where bar is not a string, then .net will attempt to convert that to a string for you. This, already, is not a good idea. You don’t want .net to control your serialization unless you’re absolutely sure it does it correct both ways. Another thing to dislike about it is that it expects the first parameter to be a string or to convert to one. It is implicit. Not explicit. Nowadays we’d use a NameValueCollection to do this, but those weren’t around when Module Settings were first invented. So tough luck. You’re going to have to keep an eye on those conversions yourself.
Now when we add or draw out information from our settings we use that first parameter as identifier. Duh. That’s simple enough. So
FacebookAppId = CStr(Settings(“FacebookAppId”))
Is what you’d typically see. Note you get the ugly conversion bits floating around here as the hastable returns objects (unless you are programming with option strict off, which I’d never do). Of course this doesn’t work when the property bag is empty or this value has not been added yet. You’ll get a big fat error as null doesn’t convert to a string. So typically we’d see this:
If Not Settings("xmlsrc") Is Nothing
xmlsrc = CType(Settings("xmlsrc"), String)
End If
This is pretty bulky to just retrieve a setting. And note you are writing the name of the setting twice in your code. Which is a pity as it violates the rule “keep the amount of times you write a key as a plain string in your code to a bare minimum”. Reread that and if necessary post it above your monitor. You would not be the first one to spend hours debugging an issue that was caused by a misspelt key somewhere. I have worked on code where the above pattern was repeated across several ascxs in the same project and even in several methods within those ascxs. This explodes the number of times you’re writing your key value.
Step 1: Taking the settings aside in their own class
The first thing to do is to do away with this diarrhea of hastable conversions across your code. Get all of this abstracted into your own settings class. Your class will have regular properties with their types instead of dealing with the flux of objects and implicit conversions. This is simple enough. You get something like this:
Public Class ModuleSettings
Private _moduleId As Integer = -1
Private _DefaultCacheTime As Integer = 30
Public Property DefaultCacheTime() As Integer
Get
Return _DefaultCacheTime
End Get
Set(ByVal Value As Integer)
_DefaultCacheTime = Value
End Set
End Property
Public Sub New(ByVal ModuleId As Integer)
_moduleId = ModuleId
Dim settings As HashTable = (New DotNetNuke.Entities.Modules.ModuleController).GetModuleSettings(ModuleId)
If settings("DefaultCacheTime") IsNot Nothing Then
DefaultCacheTime = CInt(settings("DefaultCacheTime"))
End If
End Sub
Public Sub Save()
Dim objModules As New DotNetNuke.Entities.Modules.ModuleController
objModules.UpdateModuleSetting(_moduleId, "DefaultCacheTime", Me.DefaultCacheTime.ToString)
End Sub
End Class
This is already a lot better. Here we see that now we can use the property DefaultCacheTime in our code. As a bonus: it has been primed with a value in case there are no settings! We are now half way to enlightenment. We have now reduced the occurrence of “DefaultCacheTime” as a string to 3. But we can do better as we’ll see later. But first let’s talk inheritance.
Step 2: Completely hiding the hashtable
The next step is to override the PortalModuleBase and inject a class of your own that your ascxs inherit from. This allows you to intervene and remove the ugly duckling Settings and replace it with the beautiful swan version of Settings:
Public Class Modulebase
Inherits Entities.Modules.PortalModuleBase
Private _modSettings As ModuleSettings
Public Shadows Property Settings() As ModuleSettings
Get
If _modSettings Is Nothing Then
_modSettings = ModuleSettings.GetModuleSettings(ModuleId)
End If
Return _modSettings
End Get
Set(ByVal value As ModuleSettings)
_modSettings = value
End Set
End Property
End Class
As you can see the Settings shadow the base settings. So now in your codebehind you’d write:
Settings.DefaultCacheTime
Your enlightenment is now almost complete. We will now revisit the ModuleSettings class you created.
Step 3: Perfecting the settings class
One thing I’m not that happy with is the “if settings(‘foo’) is not nothing then read its value”. I find it bulky and when you have a lot of settings it begins to really create tons and tons of code. So here’s a new radical idea: let’s draw out those values with extension methods. Check out this method:
Public Sub ReadValue(ByRef ValueTable As Hashtable, ByVal ValueName As String, ByRef Variable As Integer)
If Not ValueTable.Item(ValueName) Is Nothing Then
Try
Variable = CType(ValueTable.Item(ValueName), Integer)
Catch ex As Exception
End Try
End If
End Sub
This does the conversion based on its availability and even puts the Try-Catch in case the value was not really an integer. The result is that Variable will only be loaded if that value has been found and is a valid integer. This is what the logic should do. You can even make this a function returning a boolean if the value was read or not. For now I’ll leave it as a method. So now our code in the settings constructor becomes:
settings.ReadValue(“DefaultCacheTime”, DefaultCacheTime)
And we overload with all other data types that we need (pay especial attention to date conversions!). There is another minor benefit to this approach: now I get to see the New and Save methods on one screen in code and I can easily spot any omissions or misspellings. The code becomes really compact and we have limited the string key values to 2! One for reading and one for writing. Note: you could of course create a variable for the key name and reduce that to 1. And I’m not against that. But 2, especially when you can see on the same page, is in my opinion an adequate reduction of failure risk already.
Final Words
I should also mention IPortable. Implementing this requires you to serialize and deserialize your settings. You can now offload that code to your settings class and make sure this stays in one place.
So there you have it. This is how I approach settings. It’s typically the first thing I tackle when I get involved in an existing module project (Blog/Newsfeeds). It is not hard to do and it really cleans up a lot of code (the blog module code is still marred by other issues, but that’d be the topic of another post). Hopefully the extension methods will make it into the DNN core one day. In my Document Exchange I have now created them for various types of variables and collections. So I can also use them on Request.Params for instance. Simple and effective.