橡胶鸭传奇继续,因为我发现需要滚动我自己的配置。由于该程序实际上是一个*.dll,并且可供多个主机应用程序使用,所以使用app.config不是一种选择。我决定利用XML序列化来允许用户修改在任务列表中获取的评论。我觉得一切都开始得很好,然后当我构建UI的时候,一切都向南发展。我试图将显示令牌的关注点与令牌的实际想法分开,但不幸地失败了。我还有一个公共静态类,我对它并不满意。我闻到了反模式的味道。
首先,我创建了根Configuration类。
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;
}
}
}而UserSettings也同样是笔直的。
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;
}
}
}就像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;
}
}
}最后,实际表示字符串标记的类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;
}
}
}我对这些类的评论是开放的,但它们实际上只是为了上下文。我真正好奇的是我将Configuration加载到UI中以进行操作的方法。我使用静态ConfigurationLoader类将XML文件序列化为Configuration对象,然后将其注入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
}
}在TodoModel中,我最初只注入了标记,但结果表明它需要能够将对象序列化为XML。所以,它现在叫public static ConfigurationLoader,我闻到了一股气味。
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);
}
}
}最后但并非最不重要。下面是我的TodoSettingsControl的代码,它嵌入到表单中。
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;
}
}
}最后看起来都是这样。

发布于 2015-01-02 10:03:18
我认为几个角色接口在这里会很有帮助。您需要对配置做两件事
因此:
public interface IConfigurationReader
{
Configuration ReadConfiguration();
}
public interface IConfigurationWriter
{
void WriteConfiguration<T>(T configuration);
}很明显,这些名字还可以改进。然后,您可以创建一个实现两者的类:
public class XmlConfigurationStore : IConfigurationReader, IConfigurationWriter
{
// Implementation as before
}您还可以添加这些内容来获取默认值,但不确定这在接口上是否有意义,或者可能作为ConfigurationStore基类的一部分。
您可能希望将这些服务组合到您的控件中:
public partial class TodoListSettingsControl : UserControl
{
private readonly IConfigurationReader configurationReader;
private readonly IConfigurationWriter configurationWriter;
public TodoListSettingsControl()
{
var configStore = new XmlConfigurationStore;
configurationReader = configurationWriter = configStore;
// Anything else.
}
}我真的不同意视图模型保存自己-它让我想起了活动记录(我从来没有真正喜欢过)。它完成了工作,但我认为如果TodoListSettingsModel类只是一些数据的包装器,并且您有另一个负责保存模型的服务,那么您的代码会工作得更好。这个服务几乎可以肯定地同时依赖于IConfigurationReader和IConfigurationWriter。
发布于 2015-01-01 22:13:53
我一般不想再加第二个答案,但我要提出的建议比我之前提议的要大得多,这似乎还不够。
我从GitHub中删除了整个源代码,问题中真正缺少的是如何“注入”这些依赖项。具体来说,您正在表单的构造函数中添加大量的new,然后从那里开始。然后在后面的页面中,在一个表单事件中,有以下内容:
var markers = new List<ToDoMarker>(_config.UserSettings.ToDoListSettings.ToDoMarkers);
controlToActivate = new TodoListSettingsControl(new TodoSettingModel(markers));有几件事对我来说很突出:
Main()中滚动您自己的DI,尝试解析表单本身并将依赖项传递给它们的构造函数(不推荐)。new表单的构造函数中所需的所有内容,让它充当每个表单的引导程序。IInspections转换成一个数组,然后对数组进行迭代,以便每个项被用来实例化一个CodeInspection。您可以将内部方法的方法签名更改为只返回一个IEnumerable,并且可以使用.Select()将其投影到一个新集合中,在lambda中定义实例化。然后,如果你必须的话,你可以转换成一个列表。如果太长的话,我可以保持简单:合并方法和对象,因为你打破了太多的方法,收紧你的依赖图,让它连接在一个地方,尽可能接近于表单的实例化,并且不要在离UI更近的时候将接口向上转换/转换成具体的类型。
发布于 2014-12-24 17:34:24
嗯,我相信模型是保存配置的正确地方。我认为您关注的原因来自于ConfigurationLoader类的静态特性,我查看了它,没有发现任何设计问题。
促使我提出建议的是如何进行单元测试。你得做个权衡。一方面,如果不必每次执行ConfigurationLoader的SaveConfiguration,就可以更容易地测试模型,方法是更改代码,将其的一个实例与模型构造函数中的标记一起注入,并模拟SaveConfiguration方法。另一方面,ConfigurationLoader的行为更像是一个内部类,如果您不让内部元素对您的测试项目可见,那么您就失去了能够直接在方法上运行测试的粒度。
我的建议是使ConfigurationLoader成为一个内部类,并让您的工作在应用程序的生命周期内执行注入、管理它的一个实例。这对你有用吗?
https://codereview.stackexchange.com/questions/74782
复制相似问题