This is a follow up to Rolling my own Configuration with UI. I got a lot of great advice about the Configuration system I created, but no one touched on how all of my logic for the UI was in the code behind of the control. I decided that it was a bad idea, and took a stab at the Model-View-Controller pattern. Mostly, because I didn't like that none of that code was testable.
The code I would like to have reviewed is responsible for displaying a list of tokens to the user and allow the user to add/remove the tokens, as well as adjust their priority. (If you're curious, the tokens are used by another process as markers to be found in text searches.)
Did I get the pattern correct? Is this MVC? Are there any other code smells here?
My Form looks like this.
(The treeview is out of scope and stored in a different user control.) Red text gives the control names.
ITodoSettingsView:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Rubberduck.Config;
using System.ComponentModel;
using System.Windows.Forms;
namespace Rubberduck.UI.Settings
{
public interface ITodoSettingsView
{
TodoPriority ActiveMarkerPriority { get; set; }
string ActiveMarkerText { get; set; }
BindingList<ToDoMarker> TodoMarkers { get; set; }
int SelectedIndex { get; set; }
bool SaveEnabled { get; set; }
event EventHandler RemoveMarker;
event EventHandler AddMarker;
event EventHandler SaveMarker;
event EventHandler SelectionChanged;
event EventHandler TextChanged;
event EventHandler PriorityChanged;
}
}
TodoListSettingsUserControl (the view):
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 TodoListSettingsUserControl : UserControl, ITodoSettingsView
{
/// <summary> Parameterless Constructor is to enable design view only. DO NOT USE. </summary>
public TodoListSettingsUserControl()
{
InitializeComponent();
}
public TodoListSettingsUserControl(List<ToDoMarker> markers)
: this()
{
this.tokenListBox.DataSource = new BindingList<ToDoMarker>(markers);
this.tokenListBox.SelectedIndex = 0;
this.priorityComboBox.DataSource = Enum.GetValues(typeof(Config.TodoPriority));
}
public int SelectedIndex
{
get { return this.tokenListBox.SelectedIndex; }
set { this.tokenListBox.SelectedIndex = value; }
}
public bool SaveEnabled
{
get { return this.saveChangesButton.Enabled; }
set { this.saveChangesButton.Enabled = value; }
}
public TodoPriority ActiveMarkerPriority
{
get { return (TodoPriority)this.priorityComboBox.SelectedIndex; }
set { this.priorityComboBox.SelectedIndex = (int)value; }
}
public string ActiveMarkerText
{
get { return this.tokenTextBox.Text; }
set { this.tokenTextBox.Text = value; }
}
public BindingList<ToDoMarker> TodoMarkers
{
get { return (BindingList<ToDoMarker>)this.tokenListBox.DataSource; }
set { this.tokenListBox.DataSource = value; }
}
public event EventHandler SelectionChanged;
private void tokenListBox_SelectedIndexChanged(object sender, EventArgs e)
{
RaiseEvent(this, e, SelectionChanged);
}
public event EventHandler SaveMarker;
private void saveChangesButton_Click(object sender, EventArgs e)
{
RaiseEvent(this, e, SaveMarker);
}
public event EventHandler TextChanged;
private void tokenTextBox_TextChanged(object sender, EventArgs e)
{
RaiseEvent(this, e, TextChanged);
}
public event EventHandler PriorityChanged;
private void priorityComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
RaiseEvent(this, e, PriorityChanged);
}
public event EventHandler AddMarker;
private void addButton_Click(object sender, EventArgs e)
{
RaiseEvent(this, e, AddMarker);
}
public event EventHandler RemoveMarker;
private void removeButton_Click(object sender, EventArgs e)
{
RaiseEvent(this, e, RemoveMarker);
}
private void RaiseEvent(object sender, EventArgs e, EventHandler handler)
{
if (handler != null)
{
handler(this, e);
}
}
}
}
TodoSettingsController:
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 TodoSettingController
{
private ITodoSettingsView _view;
public ToDoMarker ActiveMarker
{
get { return _view.TodoMarkers[_view.SelectedIndex]; }
}
public TodoSettingController(ITodoSettingsView view)
{
_view = view;
if (_view.TodoMarkers != null)
{
_view.ActiveMarkerText = _view.TodoMarkers[0].Text;
_view.ActiveMarkerPriority = _view.TodoMarkers[0].Priority;
}
_view.SelectionChanged += SelectionChanged;
_view.TextChanged += TextChanged;
_view.AddMarker += AddMarker;
_view.RemoveMarker += RemoveMarker;
_view.SaveMarker += SaveMarker;
_view.PriorityChanged += PriorityChanged;
}
private void SaveMarker(object sender, EventArgs e)
{
//todo: add test; How? I can't click the save button. Code smell here.
var index = _view.SelectedIndex;
_view.TodoMarkers[index].Text = _view.ActiveMarkerText;
_view.TodoMarkers[index].Priority = _view.ActiveMarkerPriority;
}
private void RemoveMarker(object sender, EventArgs e)
{
_view.TodoMarkers.RemoveAt(_view.SelectedIndex);
}
private void AddMarker(object sender, EventArgs e)
{
var marker = new ToDoMarker(_view.ActiveMarkerText, _view.ActiveMarkerPriority);
_view.TodoMarkers.Add(marker);
_view.SelectedIndex = _view.TodoMarkers.Count - 1;
}
private void TextChanged(object sender, EventArgs e)
{
_view.SaveEnabled = true;
}
private void PriorityChanged(object sender, EventArgs e)
{
_view.SaveEnabled = true;
}
private void SelectionChanged(object sender, EventArgs e)
{
_view.ActiveMarkerPriority = this.ActiveMarker.Priority;
_view.ActiveMarkerText = this.ActiveMarker.Text;
_view.SaveEnabled = false;
}
public void SetActiveItem(int index)
{
_view.SelectedIndex = index;
}
}
}
TodoMarkers.cs:
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;
}
}
}
Here's an example call:
this.config = this.configService.LoadConfiguration();
var markers = this.config.UserSettings.ToDoListSettings.ToDoMarkers.ToList();
this.todoView = new TodoListSettingsUserControl(markers);
this.todoController = new TodoSettingController(this.todoView);
and everything from there is pretty much automagicly handled by the controller responding to view events except for this method to save the markers back to the Config file.
private void SaveConfig()
{
this.config.UserSettings.ToDoListSettings.ToDoMarkers = this.todoView.TodoMarkers.ToArray();
this.configService.SaveConfiguration<Configuration>(_config);
}
Unit Tests are available if requested, but this post was already quite long.
XmlAttributeAttribute
class has anAttributeName
property? So you can have Pascal case in your code and camelcase in the xml for e.g.ToDoMarker.Text
. I wouldn't bother though. \$\endgroup\$