The Rubberduck Saga continues as I find a need to roll my own configuration. Since the program is really a *.dll and available to several host applications, using app.config is not an option. I decided to leverage XML serialization to allow a user to modify which comments get picked up in the Task List. I feel like everything started out really well and then went south when I built the UI. I tried to separate the concerns of displaying the tokens and the actual idea of a token, but failed miserably. I also have a public static class that I'm not real happy with. It smells like an anti-pattern to me.
First I created a root Configuration
class.
using System;
using System.IO;
using System.Xml.Serialization;
using System.Runtime.InteropServices;
namespace Rubberduck.Config
{
[ComVisible(false)]
[XmlTypeAttribute(AnonymousType = true)]
[XmlRootAttribute(Namespace = "", IsNullable = false)]
public class Configuration
{
public UserSettings UserSettings { get; set; }
public Configuration()
{
//default constructor required for serialization
}
public Configuration(UserSettings userSettings)
{
this.UserSettings = userSettings;
}
}
}
And UserSettings
is similarly straight forward.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using System.Runtime.InteropServices;
namespace Rubberduck.Config
{
[ComVisible(false)]
[XmlTypeAttribute(AnonymousType = true)]
public class UserSettings
{
public ToDoListSettings ToDoListSettings { get; set; }
public CodeInspectionSettings CodeInspectinSettings { get; set; }
public UserSettings()
{
//default constructor required for serialization
}
public UserSettings(ToDoListSettings todoSettings, CodeInspectionSettings codeInspectionSettings)
{
this.ToDoListSettings = todoSettings;
this.CodeInspectinSettings = codeInspectionSettings;
}
}
}
As is TodoListSettings
using System.Xml.Serialization;
using System.Runtime.InteropServices;
namespace Rubberduck.Config
{
interface IToDoListSettings
{
ToDoMarker[] ToDoMarkers { get; set; }
}
[ComVisible(false)]
[XmlTypeAttribute(AnonymousType = true)]
public class ToDoListSettings : IToDoListSettings
{
[XmlArrayItemAttribute("ToDoMarker", IsNullable = false)]
public ToDoMarker[] ToDoMarkers { get; set; }
public ToDoListSettings()
{
//empty constructor needed for serialization
}
public ToDoListSettings(ToDoMarker[] markers)
{
this.ToDoMarkers = markers;
}
}
}
And finally, the class that actually represents the string tokens, TodoMarker
.
using System.Xml.Serialization;
using System.Runtime.InteropServices;
using Rubberduck.VBA.Parser;
namespace Rubberduck.Config
{
[ComVisible(false)]
public enum TodoPriority
{
Low,
Normal,
High
}
[ComVisible(false)]
public interface IToDoMarker
{
TodoPriority Priority { get; set; }
string Text { get; set; }
}
[ComVisible(false)]
[XmlTypeAttribute(AnonymousType = true)]
public class ToDoMarker : IToDoMarker
{
//either the code can be properly case, or the XML can be, but the xml attributes must here *exactly* match the xml
[XmlAttribute]
public string Text { get; set; }
[XmlAttribute]
public TodoPriority Priority { get; set; }
/// <summary> Default constructor is required for serialization. DO NOT USE. </summary>
public ToDoMarker()
{
// default constructor required for serialization
}
public ToDoMarker(string text, TodoPriority priority)
{
Text = text;
Priority = priority;
}
/// <summary> Convert this object into a string representation. Over-riden for easy databinding.</summary>
/// <returns> The Text property. </returns>
public override string ToString()
{
return this.Text;
}
}
}
I'm open to comments on these classes, but they're really just here for context. What I'm really curious about is my method of loading the Configuration
into the UI for manipulation. I'm using a static ConfigurationLoader
class to serialize the XML file into a Configuration
object and then injecting that into a TodoModel
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
using System.Runtime.InteropServices;
using System.IO;
using Rubberduck.Inspections;
using System.Reflection;
using Rubberduck.VBA.Parser.Grammar;
namespace Rubberduck.Config
{
[ComVisible(false)]
public static class ConfigurationLoader
{
private static string configFile = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\Rubberduck\rubberduck.config";
/// <summary> Saves a Configuration to Rubberduck.config XML file via Serialization.</summary>
public static void SaveConfiguration<T>(T toSerialize)
{
XmlSerializer xmlSerializer = new XmlSerializer(toSerialize.GetType());
using (TextWriter textWriter = new StreamWriter(configFile))
{
xmlSerializer.Serialize(textWriter, toSerialize);
}
}
/// <summary> Loads the configuration from Rubberduck.config xml file. </summary>
/// <remarks> If an IOException occurs returns a default configuration.</remarks>
public static Configuration LoadConfiguration()
{
try
{
using (StreamReader reader = new StreamReader(configFile))
{
var deserializer = new XmlSerializer(typeof(Configuration));
return (Configuration)deserializer.Deserialize(reader);
}
}
catch (IOException)
{
return GetDefaultConfiguration();
}
}
public static Configuration GetDefaultConfiguration()
{
var userSettings = new UserSettings(
new ToDoListSettings(GetDefaultTodoMarkers()),
new CodeInspectionSettings(GetDefaultCodeInspections())
);
return new Configuration(userSettings);
}
public static ToDoMarker[] GetDefaultTodoMarkers()
{
var note = new ToDoMarker("NOTE:", TodoPriority.Low);
var todo = new ToDoMarker("TODO:", TodoPriority.Normal);
var bug = new ToDoMarker("BUG:", TodoPriority.High);
return new ToDoMarker[] { note, todo, bug };
}
// omitted some reflection methods for a different set of configs for brevity
}
}
In TodoModel
I originally injected just the markers, but it turns out that it needs to be able to serialize the object into XML. So, it now calls the public static ConfigurationLoader
and I smell a smell.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Rubberduck.Config;
using System.ComponentModel;
namespace Rubberduck.UI.Settings
{
public class TodoSettingModel
{
private BindingList<ToDoMarker> _markers;
public BindingList<ToDoMarker> Markers { get { return _markers; } }
public TodoSettingModel(List<ToDoMarker> markers)
{
_markers = new BindingList<ToDoMarker>(markers);
}
public void Save()
{
var settings = new ToDoListSettings(_markers.ToArray());
var config = ConfigurationLoader.LoadConfiguration();
config.UserSettings.ToDoListSettings = settings;
ConfigurationLoader.SaveConfiguration<Configuration>(config);
}
}
}
Last, but not least. Here is the code behind of my TodoSettingsControl
which gets embedded into a Form.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Rubberduck.Config;
namespace Rubberduck.UI.Settings
{
public partial class TodoListSettingsControl : UserControl
{
private TodoSettingModel _model;
private IToDoMarker _activeMarker;
/// <summary> Parameterless Constructor is to enable design view only. DO NOT USE. </summary>
public TodoListSettingsControl()
{
InitializeComponent();
}
public TodoListSettingsControl(TodoSettingModel model):this()
{
_model = model;
this.tokenListBox.DataSource = _model.Markers;
this.tokenListBox.SelectedIndex = 0;
this.priorityComboBox.DataSource = Enum.GetValues(typeof(Config.TodoPriority));
SetActiveMarker();
}
private void SetActiveMarker()
{
_activeMarker = (IToDoMarker)this.tokenListBox.SelectedItem;
if (_activeMarker != null && this.priorityComboBox.Items.Count > 0)
{
this.priorityComboBox.SelectedIndex = (int)_activeMarker.Priority;
}
this.tokenTextBox.Text = _activeMarker.Text;
}
private void tokenListBox_SelectedIndexChanged(object sender, EventArgs e)
{
SetActiveMarker();
}
private void saveChangesButton_Click(object sender, EventArgs e)
{
var index = this.tokenListBox.SelectedIndex;
_model.Markers[index].Text = tokenTextBox.Text;
_model.Markers[index].Priority = (TodoPriority)priorityComboBox.SelectedIndex;
_model.Save();
}
private void tokenTextBox_TextChanged(object sender, EventArgs e)
{
this.saveChangesButton.Enabled = true;
}
private void addButton_Click(object sender, EventArgs e)
{
var marker = new ToDoMarker(this.tokenTextBox.Text, (TodoPriority)this.priorityComboBox.SelectedIndex);
_model.Markers.Add(marker);
_model.Save();
this.tokenListBox.DataSource = _model.Markers;
}
private void removeButton_Click(object sender, EventArgs e)
{
_model.Markers.RemoveAt(this.tokenListBox.SelectedIndex);
_model.Save();
this.tokenListBox.DataSource = _model.Markers;
}
}
}
Which all ends up looking like this.