diff --git a/Source/Parsing/ParserHelper.cs b/Source/Parsing/ParserHelper.cs
index 69032ae..b6dbf10 100644
--- a/Source/Parsing/ParserHelper.cs
+++ b/Source/Parsing/ParserHelper.cs
@@ -176,75 +176,6 @@ namespace Nuclex.Support.Parsing {
return false;
}
- /// Tried to parse a boolean literal
- /// Value that will be parsed as a boolean literal
- ///
- /// True or false if the value was a boolean literal, null if it wasn't
- ///
- public static bool? ParseBooleanLiteral(string value) {
- if(value == null) {
- return null;
- }
-
- var stringSegment = new StringSegment(value, 0, value.Length);
- return ParseBooleanLiteral(ref stringSegment);
- }
-
- /// Tried to parse a boolean literal
- /// Value that will be parsed as a boolean literal
- ///
- /// True or false if the value was a boolean literal, null if it wasn't
- ///
- public static bool? ParseBooleanLiteral(ref StringSegment value) {
- switch(value.Count) {
-
- // If the string spells 'no', it is considered a boolean
- case 2: {
- bool isSpellingNo =
- ((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) &&
- ((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O'));
- return isSpellingNo ? new Nullable(false) : null;
- }
-
- // If the string spells 'yes', it is considered a boolean
- case 3: {
- bool isSpellingYes =
- ((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) &&
- ((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) &&
- ((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S'));
- return isSpellingYes ? new Nullable(true) : null;
- }
-
- // If the string spells 'true', it is considered a boolean
- case 4: {
- bool isSpellingTrue =
- ((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) &&
- ((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) &&
- ((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) &&
- ((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E'));
- return isSpellingTrue ? new Nullable(true) : null;
- }
-
- // If the string spells 'false', it is considered a boolean
- case 5: {
- bool isSpellingFalse =
- ((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) &&
- ((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) &&
- ((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) &&
- ((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) &&
- ((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E'));
- return isSpellingFalse ? new Nullable(false) : null;
- }
-
- // Anything else is not considered a boolean
- default: {
- return null;
- }
-
- }
- }
-
-
}
} // namespace Nuclex.Support.Parsing
diff --git a/Source/Settings/ConfigurationFileStore.Parsing.cs b/Source/Settings/ConfigurationFileStore.Parsing.cs
index d5a2e0b..ed8b6cc 100644
--- a/Source/Settings/ConfigurationFileStore.Parsing.cs
+++ b/Source/Settings/ConfigurationFileStore.Parsing.cs
@@ -253,13 +253,67 @@ namespace Nuclex.Support.Settings {
}
// If it parses as a boolean literal, then it must be a boolean
- if(ParserHelper.ParseBooleanLiteral(ref value) != null) {
+ if(parseBooleanLiteral(ref value) != null) {
return typeof(bool);
}
return typeof(string);
}
+ /// Tried to parse a boolean literal
+ /// Value that will be parsed as a boolean literal
+ ///
+ /// True or false if the value was a boolean literal, null if it wasn't
+ ///
+ private static bool? parseBooleanLiteral(ref StringSegment value) {
+ switch(value.Count) {
+
+ // If the string spells 'no', it is considered a boolean
+ case 2: {
+ bool isSpellingNo =
+ ((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) &&
+ ((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O'));
+ return isSpellingNo ? new Nullable(false) : null;
+ }
+
+ // If the string spells 'yes', it is considered a boolean
+ case 3: {
+ bool isSpellingYes =
+ ((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) &&
+ ((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) &&
+ ((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S'));
+ return isSpellingYes ? new Nullable(true) : null;
+ }
+
+ // If the string spells 'true', it is considered a boolean
+ case 4: {
+ bool isSpellingTrue =
+ ((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) &&
+ ((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) &&
+ ((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) &&
+ ((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E'));
+ return isSpellingTrue ? new Nullable(true) : null;
+ }
+
+ // If the string spells 'false', it is considered a boolean
+ case 5: {
+ bool isSpellingFalse =
+ ((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) &&
+ ((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) &&
+ ((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) &&
+ ((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) &&
+ ((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E'));
+ return isSpellingFalse ? new Nullable(false) : null;
+ }
+
+ // Anything else is not considered a boolean
+ default: {
+ return null;
+ }
+
+ }
+ }
+
}
} // namespace Nuclex.Support.Configuration
diff --git a/Source/Settings/ConfigurationFileStore.cs b/Source/Settings/ConfigurationFileStore.cs
index 5481d1d..a6136b5 100644
--- a/Source/Settings/ConfigurationFileStore.cs
+++ b/Source/Settings/ConfigurationFileStore.cs
@@ -156,7 +156,7 @@ namespace Nuclex.Support.Settings {
Option option;
if(containingCategory.OptionLookup.TryGetValue(optionName, out option)) {
if(typeof(TValue) == typeof(bool)) {
- bool? boolean = ParserHelper.ParseBooleanLiteral(ref option.OptionValue);
+ bool? boolean = parseBooleanLiteral(ref option.OptionValue);
if(boolean.HasValue) {
value = (TValue)(object)boolean.Value;
return true;
diff --git a/Source/Settings/ISettingsStore.cs b/Source/Settings/ISettingsStore.cs
index 7bcfe28..f04b2c2 100644
--- a/Source/Settings/ISettingsStore.cs
+++ b/Source/Settings/ISettingsStore.cs
@@ -24,6 +24,27 @@ using System.Collections.Generic;
namespace Nuclex.Support.Settings {
/// Interface by which settings and configuration data can be accessed
+ ///
+ ///
+ /// The intended usage pattern for options is for your application to simply read and
+ /// write whatever options it needs using the type it expects them to be.
+ ///
+ ///
+ /// When you enumerate the options appearing under a category, the settings store will
+ /// try to guess the likely type of an option, but this is not always accurate. For
+ /// example, assigning the text 'true' to an option in a .cfg or .ini file could mean
+ /// that the option is a boolean or it could simply be a coincidence. When you read
+ /// this value as a boolean, the settings store will correctly convert it to a boolean,
+ /// when you read it as a string, you will get back "true" in plain text.
+ ///
+ ///
+ /// Which types of values a settings store can accept can also change between different
+ /// settings store implementations. The windows registry supports string and byte
+ /// arrays whereas configuration files have no standardized way of holding these.
+ /// Any property store must support a minimal subset including booleans, integers,
+ /// floating point values and strings.
+ ///
+ ///
public interface ISettingsStore {
/// Enumerates the categories defined in the configuration
diff --git a/Source/Settings/WindowsRegistryStore.Test.cs b/Source/Settings/WindowsRegistryStore.Test.cs
index 79107d4..0e4e116 100644
--- a/Source/Settings/WindowsRegistryStore.Test.cs
+++ b/Source/Settings/WindowsRegistryStore.Test.cs
@@ -23,6 +23,10 @@ License along with this library
using System;
using NUnit.Framework;
+using System.IO;
+using Microsoft.Win32;
+using System.Globalization;
+using System.Collections.Generic;
namespace Nuclex.Support.Settings {
@@ -30,7 +34,153 @@ namespace Nuclex.Support.Settings {
[TestFixture]
internal class WindowsRegistryStoreTest {
-
+ #region class TestContext
+
+ /// Sets up a temporary registry key for the unit test
+ private class TestContext : IDisposable {
+
+ /// Initializes a new test context
+ public TestContext() {
+ this.keyName = Guid.NewGuid().ToString();
+ this.registryKey = Registry.CurrentUser.CreateSubKey(this.keyName);
+ this.store = new WindowsRegistryStore(this.registryKey, writable: true);
+ }
+
+ /// Immediately frees all resources owned by the test context
+ public void Dispose() {
+ if(this.store != null) {
+ this.store.Dispose();
+ this.store = null;
+ this.registryKey = null;
+ } else if(this.registryKey != null) {
+ this.registryKey.Dispose();
+ this.registryKey = null;
+ }
+ if(this.keyName != null) {
+ Registry.CurrentUser.DeleteSubKeyTree(this.keyName);
+ this.keyName = null;
+ }
+ }
+
+ /// Store created on a temporary registry key
+ public WindowsRegistryStore Store {
+ get { return this.store; }
+ }
+
+ /// Name of the temporary registry key
+ private string keyName;
+ /// Registry key (ownership transfered to the store)
+ private RegistryKey registryKey;
+ /// Store that is accessing the registry key
+ private WindowsRegistryStore store;
+
+ }
+
+ #endregion // class TestContext
+
+ /// Verifies that new instances of the registry store can be created
+ [Test]
+ public void CanBeCreated() {
+ Assert.That(
+ () => { using(var context = new TestContext()) { } }, Throws.Nothing
+ );
+ }
+
+ /// Verifies that booleans can be stored in the registry
+ [Test]
+ public void BooleansCanBeStored() {
+ using(var context = new TestContext()) {
+ context.Store.Set(null, "test", true);
+ Assert.That(context.Store.Get(null, "test"), Is.True);
+
+ context.Store.Set(null, "test", false);
+ Assert.That(context.Store.Get(null, "test"), Is.False);
+ }
+ }
+
+ /// Verifies that integers can be stored in the registry
+ [Test]
+ public void IntegersCanBeStored() {
+ using(var context = new TestContext()) {
+ context.Store.Set(null, "test", 123);
+ Assert.That(context.Store.Get(null, "test"), Is.EqualTo(123));
+
+ context.Store.Set(null, "test", 456);
+ Assert.That(context.Store.Get(null, "test"), Is.EqualTo(456));
+ }
+ }
+
+ /// Verifies that floats can be stored in the registry
+ [Test]
+ public void FloatsCanBeStored() {
+ float testValue = float.Parse("123.456", CultureInfo.InvariantCulture);
+
+ using(var context = new TestContext()) {
+ context.Store.Set(null, "test", testValue);
+ Assert.That(context.Store.Get(null, "test"), Is.EqualTo(testValue));
+
+ testValue = float.Parse("654.321", CultureInfo.InvariantCulture);
+
+ context.Store.Set(null, "test", testValue);
+ Assert.That(context.Store.Get(null, "test"), Is.EqualTo(testValue));
+ }
+ }
+
+ /// Verifies that strings can be stored in the registry
+ [Test]
+ public void StringsCanBeStored() {
+ using(var context = new TestContext()) {
+ context.Store.Set(null, "test", "hello world");
+ Assert.That(context.Store.Get(null, "test"), Is.EqualTo("hello world"));
+
+ context.Store.Set(null, "test", "world hello");
+ Assert.That(context.Store.Get(null, "test"), Is.EqualTo("world hello"));
+ }
+ }
+
+ /// Verifies that the subkeys of a registry key can be enumerated
+ [Test]
+ public void CategoriesCanBeEnumerated() {
+ string[] names = new string[] { "one", "two", "three" };
+
+ using(var context = new TestContext()) {
+ context.Store.Set(names[0], "sol", 21);
+ context.Store.Set(names[1], "sol", 42);
+ context.Store.Set(names[2], "sol", 84);
+
+ Assert.That(context.Store.EnumerateCategories(), Is.EquivalentTo(names));
+ }
+ }
+
+ /// Verifies that the values under a registry subkey can be enumerated
+ [Test]
+ public void OptionsInCategoryCanBeEnumerated() {
+ string[] names = new string[] { "one", "two", "three" };
+
+ using(var context = new TestContext()) {
+ context.Store.Set("test", names[0], 1);
+ context.Store.Set("test", names[1], 2);
+ context.Store.Set("test", names[2], 3);
+
+ var optionInfos = new List(context.Store.EnumerateOptions("test"));
+ Assert.That(optionInfos.Count, Is.EqualTo(3));
+ }
+ }
+
+ /// Verifies that the values under a registry key can be enumerated
+ [Test]
+ public void RootOptionsCanBeEnumerated() {
+ string[] names = new string[] { "one", "two", "three" };
+
+ using(var context = new TestContext()) {
+ context.Store.Set(null, names[0], 1);
+ context.Store.Set(null, names[1], 2);
+ context.Store.Set(null, names[2], 3);
+
+ var optionInfos = new List(context.Store.EnumerateOptions(null));
+ Assert.That(optionInfos.Count, Is.EqualTo(3));
+ }
+ }
}
diff --git a/Source/Settings/WindowsRegistryStore.cs b/Source/Settings/WindowsRegistryStore.cs
index 560fd3d..9781674 100644
--- a/Source/Settings/WindowsRegistryStore.cs
+++ b/Source/Settings/WindowsRegistryStore.cs
@@ -131,8 +131,13 @@ namespace Nuclex.Support.Settings {
if(string.IsNullOrEmpty(category)) {
return tryGetValueFromKey(this.rootKey, optionName, out value);
} else {
- using(RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable)) {
- return tryGetValueFromKey(this.rootKey, optionName, out value);
+ RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable);
+ if(categoryKey == null) {
+ value = default(TValue);
+ return false;
+ }
+ using(categoryKey) {
+ return tryGetValueFromKey(categoryKey, optionName, out value);
}
}
}
@@ -143,7 +148,43 @@ namespace Nuclex.Support.Settings {
/// Name of the option that will be saved
/// The value under which the option will be saved
public void Set(string category, string optionName, TValue value) {
- throw new NotImplementedException();
+ if(string.IsNullOrEmpty(category)) {
+ setValue(this.rootKey, optionName, value);
+ } else {
+ RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable);
+ if(categoryKey == null) {
+ categoryKey = this.rootKey.CreateSubKey(category);
+ }
+ using(categoryKey) {
+ setValue(categoryKey, optionName, value);
+ }
+ }
+ }
+
+ /// Writes a setting to the registry
+ ///
+ ///
+ ///
+ ///
+ private void setValue(RegistryKey registryKey, string optionName, TValue value) {
+ if(typeof(TValue) == typeof(int)) {
+ registryKey.SetValue(optionName, value, RegistryValueKind.DWord);
+ } else if(typeof(TValue) == typeof(long)) {
+ registryKey.SetValue(optionName, value, RegistryValueKind.QWord);
+ } else if(typeof(TValue) == typeof(bool)) {
+ registryKey.SetValue(optionName, value, RegistryValueKind.DWord);
+ } else if(typeof(TValue) == typeof(string)) {
+ registryKey.SetValue(optionName, value, RegistryValueKind.String);
+ } else if(typeof(TValue) == typeof(string[])) {
+ registryKey.SetValue(optionName, value, RegistryValueKind.MultiString);
+ } else if(typeof(TValue) == typeof(byte[])) {
+ registryKey.SetValue(optionName, value, RegistryValueKind.Binary);
+ } else {
+ string valueAsString = (string)Convert.ChangeType(
+ value, typeof(string), CultureInfo.InvariantCulture
+ );
+ registryKey.SetValue(optionName, valueAsString, RegistryValueKind.String);
+ }
}
/// Removes the option with the specified name
@@ -167,22 +208,6 @@ namespace Nuclex.Support.Settings {
if(valueAsObject == null) {
value = default(TValue);
return false;
- }
-
- if(typeof(TValue) == typeof(bool)) {
- string valueAsString = (string)Convert.ChangeType(
- valueAsObject, typeof(string), CultureInfo.InvariantCulture
- );
-
- bool? boolean = ParserHelper.ParseBooleanLiteral(valueAsString);
- if(boolean.HasValue) {
- value = (TValue)(object)boolean.Value;
- return true;
- } else {
- throw new FormatException(
- "The value '" + valueAsString + "' can not be intepreted as a boolean"
- );
- }
} else {
value = (TValue)Convert.ChangeType(
valueAsObject, typeof(TValue), CultureInfo.InvariantCulture
@@ -227,11 +252,6 @@ namespace Nuclex.Support.Settings {
}
}
- // If it parses as a boolean literal, then it must be a boolean
- if(ParserHelper.ParseBooleanLiteral(value) != null) {
- return typeof(bool);
- }
-
return typeof(string);
}