One of the things I’d been dreading implementing in WikiBench is state persistence. This is, however, very important. When the user closes the application, they should find it in the same state they left it in when they open it later.
This is a tricky one to get right, though. Because one of the goals is portability, I can’t rely on certain paths to exist. Fortunately the CLI has a nifty answer to this one. A simple call to Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
will return a user-specific path where configuration files can be saved. On *nix this is $HOME/.config
and on Windows it’s %APPDATA%
, which is typically %USERPROFILE%\Application Data
.
The format of the configuration file should also be portable. XML was the natural choice.
Then I had to deal with how to let addins hook into configuration loading and saving process. If I were going for pure efficiency I would have forced addins to deal with XmlReaders and XmlWriters. This would be painful at best. While more of a memory hog, I decided to just maintain an XmlDocument in memory.
I went through several design ideas (one of which I implemented and later had to remove) before deciding on a refactoring of the IAddinEntry interface. The initial goal of this interface was to provide a way for addins to execute code immediately after being loaded, before anything else happened, and then to clean up after the addin was unloaded. The more I thought about it, the more this seemed like the perfect place to deal with configuration data.
So I converted this interface to a class. It retained its two main methods, Enter() and Exit(). I added a property that can be used by subclasses to get the XmlElement that encloses all of the addin’s data. I also added another method, UpdateConfig(). This is called just before the configuration file is written, and is guaranteed to be called only after a call to Enter(), but before a call to Exit(), which is important for addins that do set-up and tear-down of structures that need to be persisted.
So there are just two events an addin needs to deal with to persist state: Enter(), where the state is restored, and UpdateConfig(), where the state is saved. Exit() is intentionally not involved in state persistence.
This system allows a great deal of flexibility; addins can either push updates to the configuration document as they happen, or wait for WikiBench to notify them of the impending save and store their data then. Which is the appropriate technique will depend on how the addin works.
Three bits of WikiBench now use this system. WikipediaChangeStream persists the list of wikis to stream changes from, the Recent Changes pad persists the filter selection, and the Blacklist pad persists the items in the blacklist.
Just a point worth mentioning…
The .NET Framework Design Guidelines suggest _not_ exposting XmlDocument/etc. from public members (e.g. property types, method parameters). Instead, use System.Xml.XPath.IXPathNavigable. This is implemented by XmlDocument, so you can still use XmlDocument as the backing store, but it doesn’t _require_ XmlDocument as the backing store; any other data structure could be used in the future.
This permits more flexibility for versioning purposes and future changes.
For read access I can see this, but how would this design allow an addin to modify the tree when saving state?
IXPathNavigable.CreateNavigator() returns a System.Xml.XPath.XPathNavigator instance. Under .NET 2.0, you can use XPathNavigator’s InnerXml and OuterXml properties, as well as obtain access to an XmlWriter for inserting arbitrary XML via the AppendChild(), AppendChildElement(), CreateAttribute(), ReplaceRange(), InsertAfter(), InsertBefore(), PrependChild(), etc. methods.
The XPathNavigator.CanEdit property lets you know if editing is supported. It is by XmlDocument-backed XPathNavigators, but is not by XPathDocument-backed XPathNavigators.