DNN Blog

Jan 8

Posted by: Peter Donker
1/8/2010  RssIcon

This is an old hobby horse of mine and when working on someone else’s module it’s one of the first things I do. As I’ve just reworked the Blog module source code I thought I’d blog it so others may learn from it and use it to their own benefit.

What is the issue?

DNN offers a property bag for a module to store its settings. The Settings are exposed as a hashtable through the PortalModuleBase class which is the default underlying layer for your module’s controls. This hashtable is retrieved from SQL through GetModuleSettings which gets all serialized (i.e. written as strings) settings from the ModuleSettings table in SQL. You save settings by writing them to this table using UpdateModuleSetting. A similar pattern is used for TabModuleSettings. This typically results in the following code blocks:

Reading a value:

Dim max as Integer = CType(Settings("RecentEntriesMax"), Integer)

First time loading a value:

If TabModuleSettings("RecentEntriesMax") IsNot Nothing Then
 txtMaxCount.Text = CType(TabModuleSettings("RecentEntriesMax"), String)
Else
 txtMaxCount.Text = "10"
End If

Saving a value:

Dim objModules As New Entities.Modules.ModuleController
With objModules
 If Utility.IsInteger(txtMaxCount.Text) Then
  .UpdateTabModuleSetting(TabModuleId, "RecentEntriesMax", txtMaxCount.Text)
 End If
End With

The problems I’d like to address here are:

The use of a text literal throughout your code to reference a value. This is bad practice as it created an accident waiting to happen. In the example “RecentEntriesMax” is used across various controls. This means that not only do you need to have a mental note of all the strings you’ve been using but also that any error in the string (e.g. “RecentEntryMax”) would not result in an error thrown by the compiler.

The variable is initialized across your code. We need to cater for the case where RecentEntriesMax has not yet been added to the table. When a module is first instantiated the settings for it are empty. In the example the default value is 10. The first line in the example where the value is read will throw an error if there is no value for “RecentEntriesMax”. Again, you won’t notice the error until runtime. Thus this is bad practice.

The variables are not typed. In the example RecentEntriesMax is an Integer. But a value from a hashtable is an object. So conversion happens all over your code. Again this can lead to errors at runtime and should be avoided.

Solution

For a module’s settings I always use a separate class. This class encapsulates all the above into a single entity. Meaning:

  1. All string literals used to reference the variable are only used in this class
  2. All variables are initialized inside this class
  3. All variables only leave the class as typed variables

Finally I add a static method to create the class and have it cached. The above example was refactored into the following code:

   1:   Public Class RecentEntriesSettings
   2:   
   3:  #Region " Private Members "
   4:    Private _allSettings As Hashtable
   5:    Private _tabModuleId As Integer = -1
   6:    Private _RecentEntriesTemplate As String = ""
   7:    Private _RecentEntriesMax As Integer = 10
   8:  #End Region
   9:   
  10:  #Region " Constructors "
  11:    Public Sub New(ByVal TabModuleId As Integer)
  12:   
  13:     _tabModuleId = TabModuleId
  14:     _allSettings = (New DotNetNuke.Entities.Modules.ModuleController).GetTabModuleSettings(_tabModuleId)
  15:     Globals.ReadValue(_allSettings, "RecentEntriesTemplate", RecentEntriesTemplate)
  16:     Globals.ReadValue(_allSettings, "RecentEntriesMax", RecentEntriesMax)
  17:   
  18:    End Sub
  19:   
  20:    Public Shared Function GetRecentEntriesSettings(ByVal TabModuleId As Integer) As RecentEntriesSettings
  21:     Dim CacheKey As String = "RecentEntriesSettings" & TabModuleId.ToString
  22:     Dim bs As RecentEntriesSettings = CType(DotNetNuke.Common.Utilities.DataCache.GetCache(CacheKey), RecentEntriesSettings)
  23:     If bs Is Nothing Then
  24:      bs = New RecentEntriesSettings(TabModuleId)
  25:      DotNetNuke.Common.Utilities.DataCache.SetCache(CacheKey, bs)
  26:     End If
  27:     Return bs
  28:    End Function
  29:  #End Region
  30:   
  31:  #Region " Public Members "
  32:    Public Overridable Sub UpdateSettings()
  33:   
  34:     Dim objModules As New DotNetNuke.Entities.Modules.ModuleController
  35:     With objModules
  36:      .UpdateTabModuleSetting(_tabModuleId, "RecentEntriesTemplate", RecentEntriesTemplate)
  37:      .UpdateTabModuleSetting(_tabModuleId, "RecentEntriesMax", RecentEntriesMax.ToString)
  38:     End With
  39:     Dim CacheKey As String = "RecentEntriesSettings" & _tabModuleId.ToString
  40:     DotNetNuke.Common.Utilities.DataCache.RemoveCache(CacheKey)
  41:   
  42:    End Sub
  43:  #End Region
  44:   
  45:  #Region " Properties "
  46:    Public Property RecentEntriesTemplate() As String
  47:     Get
  48:      If _RecentEntriesTemplate = "" Then
  49:       _RecentEntriesTemplate = Localization.GetString("DefaultRecentEntriesTemplate", BlogModuleBase.BLOG_TEMPLATES_RESOURCE)
  50:      End If
  51:      Return _RecentEntriesTemplate
  52:     End Get
  53:     Set(ByVal value As String)
  54:      _RecentEntriesTemplate = value
  55:     End Set
  56:    End Property
  57:   
  58:    Public Property RecentEntriesMax() As Integer
  59:     Get
  60:      Return _RecentEntriesMax
  61:     End Get
  62:     Set(ByVal value As Integer)
  63:      _RecentEntriesMax = value
  64:     End Set
  65:    End Property
  66:  #End Region
  67:   
  68:   End Class

In the example I’d like you to follow the RecentEntriesMax variable. Note how it’s initialized on line 7, retrieved on line 16, and written on line 37. Outside this class no one needs to use the string literal any longer and all code can assume the variable has been initialized. What is still missing from the above are the reading methods. The reader needs to handle the case where the value is not yet in the hashtable. Using overloading we can handle all variable types with a single method signature. So these look like this:

   1:   Public Shared Sub ReadValue(ByRef ValueTable As Hashtable, ByVal ValueName As String, ByRef Variable As Integer)
   2:    If Not ValueTable.Item(ValueName) Is Nothing Then
   3:     Try
   4:      Variable = CType(ValueTable.Item(ValueName), Integer)
   5:     Catch ex As Exception
   6:     End Try
   7:    End If
   8:   End Sub
   9:   
  10:   Public Shared Sub ReadValue(ByRef ValueTable As Hashtable, ByVal ValueName As String, ByRef Variable As Long)
  11:    If Not ValueTable.Item(ValueName) Is Nothing Then
  12:     Try
  13:      Variable = CType(ValueTable.Item(ValueName), Long)
  14:     Catch ex As Exception
  15:     End Try
  16:    End If
  17:   End Sub
  18:   
  19:   Public Shared Sub ReadValue(ByRef ValueTable As Hashtable, ByVal ValueName As String, ByRef Variable As String)
  20:    If Not ValueTable.Item(ValueName) Is Nothing Then
  21:     Try
  22:      Variable = CType(ValueTable.Item(ValueName), String)
  23:     Catch ex As Exception
  24:     End Try
  25:    End If
  26:   End Sub
  27:   
  28:   Public Shared Sub ReadValue(ByRef ValueTable As Hashtable, ByVal ValueName As String, ByRef Variable As Boolean)
  29:    If Not ValueTable.Item(ValueName) Is Nothing Then
  30:     Try
  31:      Variable = CType(ValueTable.Item(ValueName), Boolean)
  32:     Catch ex As Exception
  33:     End Try
  34:    End If
  35:   End Sub
  36:   
  37:   Public Shared Sub ReadValue(ByRef ValueTable As Hashtable, ByVal ValueName As String, ByRef Variable As Date)
  38:    If Not ValueTable.Item(ValueName) Is Nothing Then
  39:     Try
  40:      Variable = CType(ValueTable.Item(ValueName), Date)
  41:     Catch ex As Exception
  42:     End Try
  43:    End If
  44:   End Sub

Etc etc. If not already in the DNN core API (couldn’t find them when I last looked for them), I’ll propose these methods to be added there.

Final touches

To put the final touches to this I shadow the Settings property of the PortalModuleBase in my own PortalModuleBase. This would look something like this (this is from a different example using ModuleSettings, not TabModuleSettings):

   1:  Public MustInherit Class PortalModuleBase
   2:   Inherits DotNetNuke.Entities.Modules.PortalModuleBase
   3:   
   4:  #Region " Private Members "
   5:   Private _settings As Settings.Settings
   6:  #End Region
   7:   
   8:  #Region " Properties "
   9:   Public Shadows Property Settings() As Settings.Settings
  10:    Get
  11:   
  12:     If _settings Is Nothing Then
  13:      _settings = New Settings.Settings(ModuleId)
  14:     End If
  15:   
  16:     Return _settings
  17:   
  18:    End Get
  19:    Set(ByVal Value As Settings.Settings)
  20:     _settings = Value
  21:    End Set
  22:   End Property
  23:  #End Region
  24:   
  25:  End Class

Now my controls inherit from that and all have hard typed and named settings.

Conclusion

Using the above pattern you solidify your code concerning the module’s settings. Adding a new variable is quite simple (just add a property and add relevant bits of code in the reading and writing methods). You have isolated all settings stuff into a single file which should be easy to maintain.

Tags:
Categories:
Location: Blogs Parent Separator Peter Donker

6 comment(s) so far...


Gravatar

Re: Abstracting your module’s settings into their own class


Pretty nice. I would suggest that you modify your ReadValue sub so you only have one. Like so:

ReadValue(ByRef ValueTable As Hashtable, ByVal ValueName As String, ByRef Variable As Object, byVal VariableType as Integer)

Then have a VariableType enum for your different types (String, Integer, Date etc..) and just use a case statement inside your code.

By Robert on   1/8/2010
Gravatar

Re: Abstracting your module’s settings into their own class

Great stuff Peter - the Settings page has always annoyed me (Nik K. wrote a great automated settings page almost half a decade ago!) - but I always cut and paste due to time constraints.. it's time I refactored!

By Rodney Joyce on   1/8/2010
Gravatar

Re: Abstracting your module’s settings into their own class

I also always use a custom module configuration class with the following additions:

1. Use generic method for obtaining setting from settings hash by key, substituting default value if setting does not exist:

Private Function GetSetting(Of T)(ByVal key As String, ByVal defaultVaue As T) As T
Dim obj As Object = _Settings(key)
If obj Is Nothing OrElse CStr(obj) = String.Empty Then
Return defaultVaue
Else
If TypeOf defaultVaue Is System.Enum Then
Return CType([Enum].Parse(GetType(T), CStr(obj)), T)
Else
Return CType(obj, T)
End If
End If
End Function

2. Cache the custom configuration class object in my module control base class (which inherits from PortalModuleBase).

3. Define in the costom configuration class any read only properties (such as template or shared resources folder) used by all controls in the module

By William Severance on   1/8/2010
Gravatar

Re: Abstracting your module’s settings into their own class

I actually wrote a suggestion for this about 3-4 years ago and logged it in Gemini! I think the old school method of having a collection full of settings needs to be replaced by a more featurefull settings class. Here's the code for how I do mine with a mini-example. You'll notice that I use generics and type conversion which allows me to persist more interesting information to my settings and use things like enums instead of raw values.

namespace FormFactor.Config {
public class SettingManager {
public DotNetNuke.Entities.Modules.PortalModuleBase Parent { get; private set; }
public SettingManager(DotNetNuke.Entities.Modules.PortalModuleBase parent) {
Parent = parent;
}

private const int DefaultFormID_DEFAULT = -1;
private const string DefaultFormID_INDEX = "DefaultFormID";
public int DefaultFormID {
get {
return GetValue(DefaultFormID_INDEX, DefaultFormID_DEFAULT);
}
set {
SetValue(DefaultFormID_INDEX, value);
}
}


private const ConnectionStringType ConnectionString_Type_DEFAULT = ConnectionStringType.WebConfigEntry;
private const string ConnectionString_Type_INDEX = "ConnectionString_Type";
public ConnectionStringType ConnectionString_Type {
get {
return GetValue(ConnectionString_Type_INDEX, ConnectionString_Type_DEFAULT);
}
set {
SetValue(ConnectionString_Type_INDEX, value);
}
}

private const string ConnectionString_Value_DEFAULT = "SiteSqlServer";
private const string ConnectionString_Value_INDEX = "ConnectionString_Value";
public string ConnectionString_Value{
get {
return GetValue(ConnectionString_Value_INDEX, ConnectionString_Value_DEFAULT);
}
set {
SetValue(ConnectionString_Value_INDEX, value);
}
}

public string ConnectionString_Evaluate() {
string ret = "";

if (ConnectionString_Type == ConnectionStringType.WebConfigEntry) {
string Section = ConnectionString_Value;
if (Section == "") {
Section = "SiteSQLServer";
}
ret = DotNetNuke.Common.Utilities.Config.GetConnectionString(Section);
} else if(ConnectionString_Type == ConnectionStringType.StringValue) {
ret = ConnectionString_Value;
}

return ret;
}



private T GetValue(string key, T defaultvalue) {
T ret = default(T);
object value = Parent.Settings[key];
if (value == null) {
ret = defaultvalue;
} else {
var TypeConverter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(T));
try {
ret = (T)TypeConverter.ConvertFrom(value);
} catch {
ret = defaultvalue;
}

}

return ret;
}


private void SetValue(string key, object value) {
Parent.Settings[key] = value;
var MC = new DotNetNuke.Entities.Modules.ModuleController();
MC.UpdateModuleSetting(Parent.ModuleId, key, value.ToString());
}

}
}

By Tony Valenti on   1/9/2010
Gravatar

Re: Abstracting your module’s settings into their own class

Peter

I did something similar in the Host settings area a couple of releases ago.

By Charles Nurse on   1/11/2010
Gravatar

Re: Abstracting your module’s settings into their own class

A few things in "Settings" annoyed me:
- Problems of using strings instead of compiled names and their retrieving their default value (as described by peter)
- Writing too many UpdateSettings

So I designed my own Settings class which is not a collection (it has custom properties of the specific module), Before saving them, I serialize them into xml string, and by one command update the whole settings.
another benefit is that now I can store simple/complex but small data structures whitin module settings so there is no need to any data table:) .
The problem is settings column width is not large enough to hold a serialized xml string,so I developed a 3 column table to store my module's settings.





- Saving small/simple data structures into data tables

By Faramarz Zabihian on   1/21/2010
Attend A Webinar
Free Demo Site
Download DotNetNuke Professional Edition Trial
Have Someone Contact Me

Like Us on Facebook Join our Network on LinkedIn Follow DNN Corporate on Twitter Follow DNN on Twitter

Advertisers

Sponsors

DotNetNuke Corporation

DotNetNuke Corp. is the steward of the DotNetNuke open source project, the most widely adopted Web Content Management Platform for building web sites and web applications on Microsoft .NET. Organizations use DotNetNuke to quickly develop and deploy interactive and dynamic web sites, intranets, extranets and web applications. The DotNetNuke platform is available in a free Community and subscription-based Professional and Enterprise Editions with an Elite Support option. DotNetNuke Corp. also operates Snowcovered.com where users purchase third party apps for the platform.