Skip to content

Instantly share code, notes, and snippets.

@SCullman
Last active August 29, 2015 14:22
Show Gist options
  • Save SCullman/4739e2aa14c089424ba4 to your computer and use it in GitHub Desktop.
Save SCullman/4739e2aa14c089424ba4 to your computer and use it in GitHub Desktop.
Typed Settings for DNN
using DotNetNuke.Common;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Portals;
using System;
using System.Collections;
using System.Collections.Generic; { get { return Get(); } set { Set(value) } }
using System.Globalization;
using System.Runtime.CompilerServices;
namespace AnyNamespace
{
public class ModuleScopedSettings : StringBasedSettings
{
public ModuleScopedSettings(int moduleId, Hashtable moduleSettings)
: base(
name => moduleSettings[name] as string,
(name, value) => new ModuleController().UpdateModuleSetting(moduleId, name, value)
) { }
}
public class TabModuleScopedSettings : StringBasedSettings
{
internal static void UpdateSetting(int moduleId, int tabModuleId, string name, string value)
{
if (IsTabModuleSetting (name))
new ModuleController().UpdateTabModuleSetting(tabModuleId, SettingName(name), value);
else
new ModuleController().UpdateModuleSetting(moduleId, name, value);
}
internal static bool IsTabModuleSetting(string name)
{
return name != "Tab" && name.StartsWith("Tab") && Char.IsUpper(name[3]);
}
internal static string SettingName(string name)
{
return IsTabModuleSetting (name) ? name.Substring(3) : name);
}
public TabModuleScopedSettings(int moduleId, int tabModuleId, Hashtable moduleSettings)
: base(
name => moduleSettings[SettingName(name)] as string,
(name, value) => UpdateSetting(moduleId, tabModuleId, name, value)
) { }
}
public class PortalScopedSettings : StringBasedSettings
{
public PortalScopedSettings(int portalId)
: base(
name => PortalController.GetPortalSetting(name, portalId, ""),
(name, value) => PortalController.UpdatePortalSetting(portalId, name, value, true)
)
{ }
}
}
using DotNetNuke.Common;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
namespace AnyNameSpace
{
public interface ISettingsStore
{
T Get<T>(T @default = default(T), [CallerMemberName] string name = null);
void Set<T>(T value, [CallerMemberName] string name = null);
void Save();
}
public class StringBasedSettings : ISettingsStore
{
Dictionary<string, string> _cache;
Dictionary<string, Action> _dirty;
Func<string, string> _get;
Action<string, string> _set;
/// <param name="get">Function to get a value out of your setting store</param>
/// <param name="set">Action to save a value back to the setting store</param>
public StringBasedSettings(Func<string, string> get, Action<string, string> set)
{
Requires.NotNull("Get", get);
Requires.NotNull("Set", set);
_get = get;
_set = set;
_cache = new Dictionary<string, string>();
_dirty = new Dictionary<string, Action>();
}
// All changes to the settings are recorded and get only executed on demand
public void Save()
{
foreach (var save in _dirty.Values) save();
_dirty.Clear();
}
// CallerMemberName is used for the name of the setting. No more magic strings
public string Get(string @default = default(string), [CallerMemberName] string name = null)
{
Requires.NotNull("Name", name);
if (_cache.ContainsKey(name))
{
return _cache[name];
}
else
{
var value = _get(name) ?? @default;
_cache[name] = value;
return value;
}
}
public T Get<T>(T @default = default(T), [CallerMemberName] string name = null)
{
//required to behave Get and Get<string> the same
if (typeof(T) == typeof(string)) return (T)(object)Get((string)(object)@default, name);
var converter = TypeDescriptor.GetConverter(typeof(T));
Requires.NotNull("Converter for T", converter);
try
{
var defaultValueAsString = converter.ConvertToInvariantString(@default);
string value = Get(defaultValueAsString, name);
return (T)converter.ConvertFromInvariantString(value);
}
catch
{
return @default;
}
}
public void Set(string value, [CallerMemberName] string name = null)
{
Requires.NotNull("Name", name);
var modified = !(_cache.ContainsKey(name) && _cache[name] == value);
if (modified)
{
_cache[name] = value;
_dirty[name] = () => _set(name, value);
};
}
public void Set<T>(T value, [CallerMemberName] string name = null)
{
//required to behave Set and Set<string> the same
if (typeof(T) == typeof(string))
Set((string)(object)value, name);
else
{
var converter = TypeDescriptor.GetConverter(typeof(T));
Requires.NotNull("Converter for T", converter);
Set(converter.ConvertToInvariantString(value), name);
}
}
}
}
using dgzfp.tagung.dnn;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using FluentAssertions;
public class StringBasedSettingsTests
{
class SutClass : AnyNamespace.StringBasedSettings
{
public SutClass(Func<string, string> get, Action<string, string> set) : base(get, set) { }
public SutClass() : base(_get_dummy, _set_dummy) { }
public string AString { get { return Get(); } set { Set(value); } }
public int AnInt { get { return Get<int>(); } set { Set(value); } }
public bool ABool { get { return Get<bool>(); } set { Set(value); } }
public DateTime ADate { get { return Get<DateTime>(); } set { Set(value); } }
}
static Func<string, string> _get_dummy = name => null; //returns null
static Action<string, string> _set_dummy = (name, value) => { };//does nothing
[Fact]
public void Properties_can_be_initialized_from_external_getter()
{
Func<string, string> identity = name => name;
var sut = new SutClass(identity, _set_dummy);
sut.AString.Should().Be("AString");
}
[Fact]
public void External_setter_is_called_when_a_property_was_changed_and_saved()
{
var counter = 0;
Action<string, string> _set_counter = (name, value) => counter++;
var sut = new SutClass(_get_dummy, _set_counter);
sut.AString = "";
counter.Should().Be(0);
sut.Save();
counter.Should().Be(1);
}
[Fact]
public void Changing_multiple_properties_a_few_times_to_different_values_should_only_execute_setter_the_last_ones_on_save()
{
var counter = 0;
Action<string, string> _set_counter = (name, value) => counter++;
var sut = new SutClass(_get_dummy, _set_counter);
sut.AString = "A";
sut.AnInt = 1;
sut.AString = "B";
sut.AnInt = 2;
counter.Should().Be(0);
sut.Save();
counter.Should().Be(2);
}
[Fact]
public void Updates_to_properties_without_modifications_are_getting_ignored()
{
var counter = 0;
Action<string, string> _set_counter = (name, value) => counter++;
var sut = new SutClass(_get_dummy, _set_counter);
sut.AString = "A";
sut.AnInt = 1;
sut.Save();
counter.Should().Be(2);
sut.AString = "A";
sut.AnInt = 2;
sut.Save();
counter.Should().Be(3);
}
[Fact]
public void External_setter_is_called_only_once_per_change()
{
var counter = 0;
Action<string, string> _set_counter = (name, value) => counter++;
var sut = new SutClass(_get_dummy, _set_counter);
sut.AString = "";
sut.Save();
counter = 0;
sut.Save();
counter.Should().Be(0);
}
[Fact]
public void It_can_set_and_read_string_properties()
{
var input = "dnn-connect";
var sut = new SutClass();
sut.AString = input;
sut.AString.Should().Be(input);
}
[Fact]
public void It_can_set_and_read_integer_properties()
{
var input = 1;
var sut = new SutClass();
sut.AnInt = input;
Assert.Equal(input, sut.AnInt);
}
[Fact]
public void It_can_set_and_read_boolean_properties()
{
var input = true;
var sut = new SutClass();
sut.ABool = input;
Assert.Equal(input, sut.ABool);
}
[Fact]
public void It_can_set_and_read_date_properties()
{
var input = DateTime.Parse("06/06/2016");
var sut = new SutClass();
sut.ADate = input;
sut.ADate.Should().Be(input);
}
[Fact]
public void It_returns_default_values_if_external_getter_returns_null()
{
var sut = new SutClass();
sut.ADate.Should().Be(default(DateTime));
sut.AString.Should().Be(default(string));
sut.AnInt.Should().Be(default(int));
sut.ABool.Should().Be(default(bool));
}
}
using DotNetNuke.Common;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Portals;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace AnyNamespace
{
// No more magic strings, setting names are retrieved directly from property names
// By convention, TabModuleScopedSettings stores properties beginning with Tab as TabModuleSetting
//
// Here we are inheriting from a base class. Quicker to write but harder to test
public class Settings : TabModuleScopedSettings
{
public Settings(int moduleId, Hashtable moduleSettings) : base(moduleId, moduleSettings) { }
//normal module setting
public bool ShowProjects { get { return Get(true); } set { Set(value); } } //Get<bool> not required as the dype can be retrieved from the default value
public string Project { get { return Get<string>(); } set { Set(value) } }
public string Description { get { return Get(); } set { Set(value) } } // Get() is a shortcut for Get<string>()
public string TableName { get { return Get(); } set { Set(value) } } // Not a TabModuleSetting as the fouth letter isn't uppercase
//tabmodulesetting
//TabProjects is saved as Project in TabModuleSettings and overrides the module setting Project.
public string TabProject { get { return Get(); } set { Set(value) } }
}
public class MyModule : PortalModuleBase
{
Settings _settings;
public new Settings Settings
{
get
{
if (_settings == null) _settings = new Settings(ModuleId, TabModuleId, base.Settings);
return _settings;
}
}
// Change Settings
public void ToggleShowProjects()
{
var current = Settings.ShowProjects;
Settings.ShowProjects = !current;
// Will only update the setting ShowProjects
Settings.Save();
}
}
}
// Please compare to
// https://github.com/DNN-Connect/Map/blob/master/Common/ModuleSettings.cs
//
// uses object composition, no inheritance from a base class is required
using System;
using System.Collections;
using DotNetNuke.Collections;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Modules;
using AnyNamespace;
namespace Connect.DNN.Modules.Map.Common
{
public class ModuleSettings
{
internal ISettingsStore _store;
public ModuleSettings(int moduleId, Hashtable settings)
{
_store = new ModuleScopedSettings(moduleId,settings);
}
public string View { get { return _store.Get("Home"); } set { _store.Set(value) } }
public double MapOriginLat { get { return _store.Get(44.1); } set { _store.Set(value) } }
public double MapOriginLong { get { return _store.Get(3.07); } set { _store.Set(value) } }
public int Zoom { get { return _store.Get(8); } set { _store.Set(value) } }
public string MapWidth { get { return _store.Get("100%"); } set { _store.Set(value) } }
public string MapHeight { get { return _store.Get("500px"); } set { _store.Set(value) } }
public string Version = typeof(ModuleSettings).Assembly.GetName().Version.ToString();
public void SaveSettings()
{
_store.Save();
}
public static ModuleSettings GetSettings(ModuleInfo ctlModule)
{
return new ModuleSettings(ctlModule.ModuleId, clModule.ModuleSettings);
}
}
}
@SCullman
Copy link
Author

Please see this gist as a kickstarter for discussions. At the time I started to copy fragments from an internal project into the gist, there was no intention for core modifications.

  • You don't need pre or postfixes for (tab)modulesettings. It might be useful for portalsettings, it might be an option within the constructor.
  • Most modules don't need tabmodulesettings at all. It was used a lot in FnL as it allowed me to setup different kind of views to the same data just my using "insert existing modules". I think this convention makes sense.
  • 90% of the modules don't need tabmodulesettings at all, modulesettings are just enough.
  • CustomConverters sounds like a nice idea for an an enhancement, should be possible. Anyway, do we need them in reality? If we need to support a type other than value types or strings, we should not hide that compexity.
  • There are still some issues. PortalSettings for example don't return null for empty/new settings, in that case the default values won't work.

For me, at least StringBasedSettings and both ModuleSettings and TabModuleSettings can make sense as part of the core.

@donker
Copy link

donker commented Jun 24, 2015

Right. For custom converters I'd love to see an example. But 99% of module settings break down as follows:

  • Strings: no need for conversion
  • Integers: trivial (de)serialization
  • Booleans: fairly trivial (de)serialization (camel cased true/false for example)
  • Floating point nrs: gets more tricky because you need to make sure it's culture neutral (comma vs decimal point)

Mostly it's the first 3 that are used with text boxes, checkboxes and dropdowns. And I agree with Stefan that we can ignore portal settings for now. I'm just focused on making module developer's life easier and this would cover the 99% use case IMHO.

Peter

@donker
Copy link

donker commented Jun 24, 2015

BTW, you could have the prefix as a property on the base class, defaulting to empty and override it when necessary, no?

@bdukes
Copy link

bdukes commented Jun 29, 2015

I've added this code as a branch in DNN's repo, so we can iterate on it via pull requests and try it out: https://github.com/dnnsoftware/Dnn.Platform/tree/research/typed-settings-API

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment