Settings in Silverlight
A few months ago I was developing a Silverlight Line of Business Application with a customer when I stumbled across the requirement to store some information over multiple user sessions - user works with the app, closes the browser and reopens the browser and works with the app again. My first idea was to store the info in a cookie. It all worked out fine until I started to enable the localization using the culture/uiculture param. Setting these results in just the culture values are found in the cookie (Ahmmm… could anybody fix this please).
The other option (the only one left) is to use the isolated store. It’s a few lines more to code than using a cookie (the reason this was my second shot) but gives you the advantage of e.g. Silverlight 3.0 out-of-browser-experience support.
I was just about to add a new item to my solution – while I was awaiting the dialog to come up – when I wondered how a developer could easily define and edit settings. Some Visual Studio support would be great. The default resources dialog for example… wait … wasn’t there some settings feature in desktop apps since .NET 2.0? Yep, it’s called “Settings”. It’s exactly what I wanted – even more as you can store values at machine level, which cannot be achieved using Silverlight (currently?).
It has an item template to easily add it to a project.
It has developer experience.
… but it has no support for Silverlight.
So I had a look at what is in the real .NET Framework. I identified a few necessary classes and their members to get settings up and running in Silverlight. I fired up the IDE again and started to write the following classes:
ApplicationSettingsBase – The abstract base class for generated settings classes.
DefaultSettingsValueAttribute – The attribute used to define a default value.
UserScopedSettingAttribute – The attribute used to differentiate a user setting from machine wide settings.
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Reflection;
using System.Xml;
namespace System.Configuration
{
public abstract class ApplicationSettingsBase
{
…
}
public class DefaultSettingValueAttribute
: Attribute
{
public object Value { get; set; }
public DefaultSettingValueAttribute(object value)
{
Value = value;
}
}
public class UserScopedSettingAttribute
: Attribute
{
}
}
The settings values will be stored in a static dictionary. The values can be accessed through the indexer that just forwards the call to the dictionary.
private static readonly Dictionary<string, object> s_values =
new Dictionary<string, object>();
private static readonly object s_valuesLock = new object();
Since Silverlight 3 supports threads we ensure the thread safe write access to the dictionary inside the indexer through the lock statement.
protected object this[string key]
{
get
{
return s_values.ContainsKey(key) ? s_values[key] : null;
}
set
{
lock (s_valuesLock)
{
if (s_values.ContainsKey(key))
{
s_values[key] = value;
}
else
{
s_values.Add(key, value);
}
}
}
}
The dictionary is filled using the static method named “Syncronized”, which is called form the class that is generated by the “SettingsSingleFileGenerator” code generator.
public static ApplicationSettingsBase Synchronized(
ApplicationSettingsBase instance)
{
lock (s_valuesLock)
{
s_values.Clear();
var type = instance.GetType();
var properties =
type.GetProperties(
BindingFlags.Public | BindingFlags.Instance);
var attributeType = typeof (DefaultSettingValueAttribute);
for (var i = 0; i < properties.Length; i++)
{
var attributes =
properties[i].GetCustomAttributes(
attributeType,
true);
if (attributes == null) continue;
var defaultAttributes =
(DefaultSettingValueAttribute[]) attributes;
s_values.Add(
properties[i].Name,
defaultAttributes[0].Value);
}
LoadFromIsolatedStore();
return instance;
}
}
It gets all default values defined by the developer by utilizing reflection, on the “DefaultValueAttribute” which decorates the properties in the generated class, on the generated class. Here is an example of a property, how it can be found in a generated settings class.
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("anonymous")]
public string UserName
{
get
{
return ((string)(this["UserName"]));
}
set
{
this["UserName"] = value;
}
}
After the default values have been assigned the method sets up a call to “LoadFromIsolatedStore”. This is the functionality that loads any persisted settings from the isolated storage.
This is the point, where it comes to the question: which format do I store the information in?
I’ve chosen the format of the real settings file (the one that is a feature for desktop apps) not of its simplicity (cough) but for the sake of compatibility. Here is an example:
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile
xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings"
CurrentProfile="(Default)"
GeneratedClassNamespace="Xyz"
GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="UserName" Type="System.String" Scope="User">
<Value Profile="(Default)">anonymous</Value>
</Setting>
</Settings>
</SettingsFile>
The next step is to read this format from a file stream located in the Silverlight isolated storage. The isolated store can be found (starting from Windows Vista) below the directory path (based on the drive where the OS is installed) “C:\Users{username}\AppData\LocalLow\Microsoft\Silverlight\is\”.
private static void LoadFromIsolatedStore()
{
using (var store =
IsolatedStorageFile.GetUserStoreForApplication())
{
if (!store.FileExists(SettingsFile)) return;
using (var fileStream =
store.OpenFile(SettingsFile, FileMode.Open))
{
When it comes to reading, writing or manipulating XML I personally prefer the XmlReader/XmlWriter over the XmlDocument because of the memory consumption – even if has not a lot to do with Object Oriented Programming (welcome to structural world again…) and it does not make such a strong matter on the client.
using (var reader = XmlReader.Create(fileStream))
{
while (reader.Read())
{
if (reader.NodeType != XmlNodeType.Element ||
reader.LocalName != "Setting") continue;
var name = string.Empty;
var type = string.Empty;
while (reader.MoveToNextAttribute())
{
switch (reader.LocalName)
{
case "Name":
name = reader.Value;
break;
case "Type":
type = reader.Value;
break;
default:
break;
}
}
To get the required information, as for instance the type of the property, the first loop (inside the read()-loop) goes through the attributes.
while(reader.NodeType != XmlNodeType.Element &&
reader.LocalName != "Value" &&
reader.Read())
{
while (reader.Depth >= 3 &&
reader.Read() &&
reader.NodeType != XmlNodeType.Text)
{
}
if (reader.NodeType != XmlNodeType.Text)
{
break;
}
object value = null;
switch (type)
{
case "String":
case "System.String":
value =
reader.ReadContentAsString();
break;
…
default:
throw new InvalidOperationException(
string.Concat(
"Type \"",
type,
"\" is not supported as a " +
"settings value."));
}
After that we convert the read value to its intended type and add or update the setting to the static dictionary.
if (value != null)
{
if (s_values.ContainsKey(name))
{
s_values[name] = value;
}
else
{
s_values.Add(name, value);
}
}
Last but not least changed Values need to be persisted. To keep the number of writes small and under control I implemented a method called “Persist” that writes all values to the settings file in the isolated storage of Silverlight.
public void Persist()
{
using (var store =
IsolatedStorageFile.GetUserStoreForApplication())
{
if (store.FileExists(SettingsFile))
{
store.DeleteFile(SettingsFile);
}
using (var fileStream =
store.OpenFile(SettingsFile, FileMode.CreateNew))
{
using (var writer = XmlWriter.Create(fileStream))
{
if (writer == null) return;
writer.WriteStartDocument();
{
writer.WriteStartElement(
"",
"SettingsFile",
"http://schemas.microsoft.com/" +
"VisualStudio/2004/01/settings");
writer.WriteAttributeString(
"CurrentProfile", "(Default)");
writer.WriteAttributeString(
"GeneratedClassNamespace",
"http://schemas.microsoft.com/" +
"VisualStudio/2004/01/settings");
writer.WriteAttributeString(
"GeneratedClassName",
"Settings");
{
writer.WriteStartElement("Profiles");
writer.WriteEndElement();
writer.WriteStartElement("Settings");
{
foreach (var key in s_values.Keys)
{
writer.WriteStartElement("Setting");
writer.WriteAttributeString(
"Name",
key);
writer.WriteAttributeString(
"Scope",
"User");
writer.WriteAttributeString(
"Type",
s_values[key] == null
? "System.String"
: s_values[key].GetType().FullName);
{
writer.WriteStartElement("Value");
writer.WriteAttributeString(
"Profile",
"(Default)");
writer.WriteString(
(string)s_values[key]);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
}
writer.WriteEndElement();
}
writer.WriteEndElement();
}
writer.WriteEndDocument();
}
}
}
}
This allows us to have (at least from the API) a standard way of storing settings in Silverlight.
I hope that Microsoft picks “settings” as a feature for the next Silverlight Version….
That’s all folks!