使用抽象和依赖注入,如果 implementation-specific 细节需要在 UI 中配置怎么办?
Using abstraction and dependency injection, what if implementation-specific details need to be configurable in the UI?
我有一个应用程序可以从输入文件中加载 client/matter 个数字的列表并将它们显示在 UI 中。这些数字是简单的 zero-padded 数字字符串,例如“02240/00106”。这里是 ClientMatter
class:
public class ClientMatter
{
public string ClientNumber { get; set; }
public string MatterNumber { get; set; }
}
我正在使用 MVVM,它使用 UI 中包含的组合根的依赖注入。有一个 IMatterListLoader
服务接口,其中的实现表示从不同文件类型加载列表的机制。为简单起见,假设应用程序仅使用一种实现,即应用程序目前不支持超过一种文件类型。
public interface IMatterListLoader
{
IReadOnlyCollection<string> MatterListFileExtensions { get; }
IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile);
}
假设在我的初始版本中,我选择了一个 MS Excel 实现来加载事项列表,如下所示:
我想允许用户在运行时配置列表开始的行号和列号,因此视图可能如下所示:
这里是 IMatterListLoader
的 MS Excel 实现:
public sealed class ExcelMatterListLoader : IMatterListLoader
{
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// load using StartRowNum and StartColNum
}
}
行号和列号是特定于 MS Excel 实现的实现细节,视图模型不知道它。然而,MVVM 规定我控制视图模型中的视图属性,所以如果我 是 这样做,它将是这样的:
public sealed class MainViewModel
{
public string InputFilePath { get; set; }
// These two properties really don't belong
// here because they're implementation details
// specific to an MS Excel implementation of IMatterListLoader.
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public ICommandExecutor LoadClientMatterListCommand { get; }
public MainViewModel(IMatterListLoader matterListLoader)
{
// blah blah
}
}
只是为了比较,这里有一个基于 ASCII 文本文件的实现,我可能会考虑用于下一个版本的应用程序:
public sealed class TextFileMatterListLoader : IMatterListLoader
{
public bool HasHeaderLine { get; set; }
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// load tab-delimited client/matters from each line
// optionally skipping the header line.
}
}
现在我没有 MS Excel 实现所需的行号和列号,但我有一个布尔标志指示 client/matter 数字是否从第一行开始(即没有header 行)或从第二行开始(即 header 行)。
我认为视图模型应该不会意识到 IMatterListLoader
实现之间的变化。我如何让视图模型完成其控制表示问题的工作,但仍然不知道某些实现细节?
这是依赖图:
您可以使用一个函数来根据接口的特定类型构造 UI 元素。
public static void ConstructUI(IMatterListLoader loader) {
Type loaderType = loader.GetType();
// Do logic based on type
}
您可以为每个 IMatterListLoader 实现提供 classes,其中包含有关表示的逻辑。 (您不想将 UI 表示逻辑与 IMatterListLoader 实现混在一起)。
根据加载程序的类型,您使用正确的 class 生成 UI 个元素。
对于要加载的每种类型的文件,您需要一个单独的视图模型。
每个视图模型为其特定的加载程序进行设置。
然后可以将这些视图模型作为依赖项传递给主视图模型,主视图模型在需要时调用每个视图模型的加载;
public interface ILoaderViewModel
{
IReadOnlyCollection<ClientMatter> Load();
}
public class ExcelMatterListLoaderViewModel : ILoaderViewModel
{
private readonly ExcelMatterListLoader loader;
public string InputFilePath { get; set; }
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public ExcelMatterListLoaderViewModel(ExcelMatterListLoader loader)
{
this.loader = loader;
}
IReadOnlyCollection<ClientMatter> Load()
{
// Stuff
loader.Load(fromFile);
}
}
public sealed class MainViewModel
{
private ExcelMatterListLoaderViewModel matterListLoaderViewModel;
public ObservableCollection<ClientMatter> ClientMatters
= new ObservableCollection<ClientMatter>();
public MainViewModel(ExcelMatterListLoaderViewModel matterListLoaderViewModel)
{
this.matterListLoaderViewModel = matterListLoaderViewModel;
}
public void LoadCommand()
{
var clientMatters = matterListLoaderViewModel.Load();
foreach (var matter in clientMatters)
{
ClientMatters.Add(matter)
}
}
}
当您向应用程序添加更多类型时,您将创建新的视图模型并将它们添加为依赖项。
我会在 IMatterListLoader
接口中添加一个 Draw()
方法。然后,您的 MainViewModel 将只调用 Draw()
,而实际的 IMatterListLoader
将向 UI 添加所需的任何参数。
这有点概念化,因为我不太熟悉 WPF,因此您可能需要更改代码以使用 UserControl 或其他东西,但逻辑是相同的。
例如,假设您有 AsciiMatterListLoader,它不需要来自客户端的输入,那么 MainViewModel 中将不会显示任何内容。但是如果加载了 ExcelMatterListLoader,MainViewModel 应该添加必要的用户输入。
public sealed class AsciiMatterListLoader : IMatterListLoader
{
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// load data with no parameters
}
public Panel Draw()
{
// Nothing needs to be drawn
return null;
}
}
public sealed class ExcelMatterListLoader : IMatterListLoader
{
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// load using StartRowNum and StartColNum
}
public Panel Draw()
{
Panel panelForUserParams = new Panel();
panelForUserParams.Height = 400;
panelForUserParams.Width = 200;
TextBox startRowTextBox = new TextBox();
startRowTextBox.Name = "startRowTextBox";
TextBox startColumnTextBox = new TextBox();
startColumnTextBox.Name = "startColumnTextBox";
panelForUserParams.Children().Add(startRowTextBox);
panelForUserParams.Children().Add(startColumnTextBox);
return panelForUserParams;
}
}
public sealed class MainViewModel
{
public string InputFilePath { get; set; }
public ICommandExecutor LoadClientMatterListCommand { get; }
public MainViewModel(IMatterListLoader matterListLoader)
{
var panel = matterListLoader.Draw();
if (panel != null)
{
// Your MainViewModel should have a dummy empty panel called "placeHolderPanelForChildPanel"
var parent = this.placeHolderPanelForChildPanel.Parent;
parent.Children.Remove(this.placeHolderPanelForChildPanel); // Remove the dummy panel
parent.Children.Add(panel); // Replace with new panel
}
}
}
您可能需要使用事件处理程序将用户输入更改传递给 IMatterListLoader,或者可能使 IMatterListLoader 成为 UserControl。
编辑
@rory.ap 是的,服务层应该不知道 UI 组件。这是我调整后的答案,其中 IMatterListLoader 只是通过使用字典作为 PropertyBag 来公开它需要的属性,而不是告诉 UI 要绘制什么。这样 UI 层完成所有 UI 工作:
public interface IMatterListLoader
{
IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile);
IDictionary<string, object> Properties { get; }
void SetProperties(IDictionary<string, object> properties);
}
public sealed class AsciiMatterListLoader : IMatterListLoader
{
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IDictionary<string, object> Properties
{
get
{
return new Dictionary<string, object>(); // Don't need any parameters for ascii files
}
}
public void SetProperties(IDictionary<string, object> properties)
{
// Nothing to do
}
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// Load without using any additional params
return null;
}
}
public sealed class ExcelMatterListLoader : IMatterListLoader
{
private const string StartRowNumParam = "StartRowNum";
private const string StartColNumParam = "StartColNum";
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
private bool havePropertiesBeenSet = false;
public IDictionary<string, object> Properties
{
get
{
var properties = new Dictionary<string, object>();
properties.Add(StartRowNumParam, (uint)0); // Give default UINT value so UI knows what type this property is
properties.Add(StartColNumParam, (uint)0); // Give default UINT value so UI knows what type this property is
return properties;
}
}
public void SetProperties(IDictionary<string, object> properties)
{
if (properties != null)
{
foreach(var property in properties)
{
switch(property.Key)
{
case StartRowNumParam:
this.StartRowNum = (uint)property.Value;
break;
case StartColNumParam:
this.StartColNum = (uint)property.Value;
break;
default:
break;
}
}
this.havePropertiesBeenSet = true;
}
else
throw new ArgumentNullException("properties");
}
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
if (this.havePropertiesBeenSet)
{
// Load using StartRowNum and StartColNum
return null;
}
else
throw new Exception("Must call SetProperties() before calling Load()");
}
}
public sealed class MainViewModel
{
public string InputFilePath { get; set; }
public ICommandExecutor LoadClientMatterListCommand { get; }
private IMatterListLoader matterListLoader;
public MainViewModel(IMatterListLoader matterListLoader)
{
this.matterListLoader = matterListLoader;
if (matterListLoader != null && matterListLoader.Properties != null)
{
foreach(var prop in matterListLoader.Properties)
{
if (typeof(prop.Value) == typeof(DateTime))
{
// Draw DateTime picker for datetime value
this.placeHolderPanelForParams.Add(new DateTimePicker() { Name = prop.Key });
}
else
{
// Draw textbox for everything else
this.placeHolderPanelForParams.Add(new TextBox() { Name = prop.Key });
// You can also add validations to the input here (E.g. Dont allow negative numbers of prop is unsigned)
// ...
}
}
}
}
public void LoadFileButtonClick(object sender, EventArgs e)
{
//Get input params from UI
Dictionary<string, object> properties = new Dictionary<string, object>();
foreach(Control propertyControl in this.placeHolderPanelForParams().Children())
{
if (propertyControl is TextBox)
properties.Add(propertyControl.Name, ((TextBox)propertyControl).Text);
else if (propertyControl is DateTimePicker)
properties.Add(propertyControl.Name, ((DateTimePicker)propertyControl).Value);
}
this.matterListLoader.SetProperties(properties);
this.matterListLoader.Load(null); //Ready to load
}
}
不确定为什么没有人建议 属性 属性和反射
只需创建一个新的Attribute
,例如:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ExposeToViewAttribute : Attribute
{
public string Name { get; set; }
public ExposeToViewAttribute([System.Runtime.CompilerServices.CallerMemberName]string name = "")
{
this.Name = name;
}
}
并确保它已添加到您的视图中
var t = matterListLoader.GetType();
var props = t.GetProperties().Where((p) => p.GetCustomAttributes(typeof(ExposeToViewAttribute), false).Any());
foreach(var prop in props)
{
var att = prop.GetCustomAttributes(typeof(ExposeToViewAttribute), true).First() as ExposeToViewAttribute;
//Add to view
}
方法不会变得更干净。
那么用法就很简单了:
[ExposeToView]
public int Something { get; set; }
[ExposeToView("some name")]
public int OtherFieldWithCustomNameThen { get; set; }
但是,如果您使用类似 WPF
的方法,还有其他可能更适合您的解决方案。
我有一个应用程序可以从输入文件中加载 client/matter 个数字的列表并将它们显示在 UI 中。这些数字是简单的 zero-padded 数字字符串,例如“02240/00106”。这里是 ClientMatter
class:
public class ClientMatter
{
public string ClientNumber { get; set; }
public string MatterNumber { get; set; }
}
我正在使用 MVVM,它使用 UI 中包含的组合根的依赖注入。有一个 IMatterListLoader
服务接口,其中的实现表示从不同文件类型加载列表的机制。为简单起见,假设应用程序仅使用一种实现,即应用程序目前不支持超过一种文件类型。
public interface IMatterListLoader
{
IReadOnlyCollection<string> MatterListFileExtensions { get; }
IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile);
}
假设在我的初始版本中,我选择了一个 MS Excel 实现来加载事项列表,如下所示:
我想允许用户在运行时配置列表开始的行号和列号,因此视图可能如下所示:
这里是 IMatterListLoader
的 MS Excel 实现:
public sealed class ExcelMatterListLoader : IMatterListLoader
{
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// load using StartRowNum and StartColNum
}
}
行号和列号是特定于 MS Excel 实现的实现细节,视图模型不知道它。然而,MVVM 规定我控制视图模型中的视图属性,所以如果我 是 这样做,它将是这样的:
public sealed class MainViewModel
{
public string InputFilePath { get; set; }
// These two properties really don't belong
// here because they're implementation details
// specific to an MS Excel implementation of IMatterListLoader.
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public ICommandExecutor LoadClientMatterListCommand { get; }
public MainViewModel(IMatterListLoader matterListLoader)
{
// blah blah
}
}
只是为了比较,这里有一个基于 ASCII 文本文件的实现,我可能会考虑用于下一个版本的应用程序:
public sealed class TextFileMatterListLoader : IMatterListLoader
{
public bool HasHeaderLine { get; set; }
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// load tab-delimited client/matters from each line
// optionally skipping the header line.
}
}
现在我没有 MS Excel 实现所需的行号和列号,但我有一个布尔标志指示 client/matter 数字是否从第一行开始(即没有header 行)或从第二行开始(即 header 行)。
我认为视图模型应该不会意识到 IMatterListLoader
实现之间的变化。我如何让视图模型完成其控制表示问题的工作,但仍然不知道某些实现细节?
这是依赖图:
您可以使用一个函数来根据接口的特定类型构造 UI 元素。
public static void ConstructUI(IMatterListLoader loader) {
Type loaderType = loader.GetType();
// Do logic based on type
}
您可以为每个 IMatterListLoader 实现提供 classes,其中包含有关表示的逻辑。 (您不想将 UI 表示逻辑与 IMatterListLoader 实现混在一起)。
根据加载程序的类型,您使用正确的 class 生成 UI 个元素。
对于要加载的每种类型的文件,您需要一个单独的视图模型。
每个视图模型为其特定的加载程序进行设置。
然后可以将这些视图模型作为依赖项传递给主视图模型,主视图模型在需要时调用每个视图模型的加载;
public interface ILoaderViewModel
{
IReadOnlyCollection<ClientMatter> Load();
}
public class ExcelMatterListLoaderViewModel : ILoaderViewModel
{
private readonly ExcelMatterListLoader loader;
public string InputFilePath { get; set; }
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public ExcelMatterListLoaderViewModel(ExcelMatterListLoader loader)
{
this.loader = loader;
}
IReadOnlyCollection<ClientMatter> Load()
{
// Stuff
loader.Load(fromFile);
}
}
public sealed class MainViewModel
{
private ExcelMatterListLoaderViewModel matterListLoaderViewModel;
public ObservableCollection<ClientMatter> ClientMatters
= new ObservableCollection<ClientMatter>();
public MainViewModel(ExcelMatterListLoaderViewModel matterListLoaderViewModel)
{
this.matterListLoaderViewModel = matterListLoaderViewModel;
}
public void LoadCommand()
{
var clientMatters = matterListLoaderViewModel.Load();
foreach (var matter in clientMatters)
{
ClientMatters.Add(matter)
}
}
}
当您向应用程序添加更多类型时,您将创建新的视图模型并将它们添加为依赖项。
我会在 IMatterListLoader
接口中添加一个 Draw()
方法。然后,您的 MainViewModel 将只调用 Draw()
,而实际的 IMatterListLoader
将向 UI 添加所需的任何参数。
这有点概念化,因为我不太熟悉 WPF,因此您可能需要更改代码以使用 UserControl 或其他东西,但逻辑是相同的。
例如,假设您有 AsciiMatterListLoader,它不需要来自客户端的输入,那么 MainViewModel 中将不会显示任何内容。但是如果加载了 ExcelMatterListLoader,MainViewModel 应该添加必要的用户输入。
public sealed class AsciiMatterListLoader : IMatterListLoader
{
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// load data with no parameters
}
public Panel Draw()
{
// Nothing needs to be drawn
return null;
}
}
public sealed class ExcelMatterListLoader : IMatterListLoader
{
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// load using StartRowNum and StartColNum
}
public Panel Draw()
{
Panel panelForUserParams = new Panel();
panelForUserParams.Height = 400;
panelForUserParams.Width = 200;
TextBox startRowTextBox = new TextBox();
startRowTextBox.Name = "startRowTextBox";
TextBox startColumnTextBox = new TextBox();
startColumnTextBox.Name = "startColumnTextBox";
panelForUserParams.Children().Add(startRowTextBox);
panelForUserParams.Children().Add(startColumnTextBox);
return panelForUserParams;
}
}
public sealed class MainViewModel
{
public string InputFilePath { get; set; }
public ICommandExecutor LoadClientMatterListCommand { get; }
public MainViewModel(IMatterListLoader matterListLoader)
{
var panel = matterListLoader.Draw();
if (panel != null)
{
// Your MainViewModel should have a dummy empty panel called "placeHolderPanelForChildPanel"
var parent = this.placeHolderPanelForChildPanel.Parent;
parent.Children.Remove(this.placeHolderPanelForChildPanel); // Remove the dummy panel
parent.Children.Add(panel); // Replace with new panel
}
}
}
您可能需要使用事件处理程序将用户输入更改传递给 IMatterListLoader,或者可能使 IMatterListLoader 成为 UserControl。
编辑
@rory.ap 是的,服务层应该不知道 UI 组件。这是我调整后的答案,其中 IMatterListLoader 只是通过使用字典作为 PropertyBag 来公开它需要的属性,而不是告诉 UI 要绘制什么。这样 UI 层完成所有 UI 工作:
public interface IMatterListLoader
{
IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile);
IDictionary<string, object> Properties { get; }
void SetProperties(IDictionary<string, object> properties);
}
public sealed class AsciiMatterListLoader : IMatterListLoader
{
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
public IDictionary<string, object> Properties
{
get
{
return new Dictionary<string, object>(); // Don't need any parameters for ascii files
}
}
public void SetProperties(IDictionary<string, object> properties)
{
// Nothing to do
}
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
// Load without using any additional params
return null;
}
}
public sealed class ExcelMatterListLoader : IMatterListLoader
{
private const string StartRowNumParam = "StartRowNum";
private const string StartColNumParam = "StartColNum";
public uint StartRowNum { get; set; }
public uint StartColNum { get; set; }
public IReadOnlyCollection<string> MatterListFileExtensions { get; set; }
private bool havePropertiesBeenSet = false;
public IDictionary<string, object> Properties
{
get
{
var properties = new Dictionary<string, object>();
properties.Add(StartRowNumParam, (uint)0); // Give default UINT value so UI knows what type this property is
properties.Add(StartColNumParam, (uint)0); // Give default UINT value so UI knows what type this property is
return properties;
}
}
public void SetProperties(IDictionary<string, object> properties)
{
if (properties != null)
{
foreach(var property in properties)
{
switch(property.Key)
{
case StartRowNumParam:
this.StartRowNum = (uint)property.Value;
break;
case StartColNumParam:
this.StartColNum = (uint)property.Value;
break;
default:
break;
}
}
this.havePropertiesBeenSet = true;
}
else
throw new ArgumentNullException("properties");
}
public IReadOnlyCollection<ClientMatter> Load(FileInfo fromFile)
{
if (this.havePropertiesBeenSet)
{
// Load using StartRowNum and StartColNum
return null;
}
else
throw new Exception("Must call SetProperties() before calling Load()");
}
}
public sealed class MainViewModel
{
public string InputFilePath { get; set; }
public ICommandExecutor LoadClientMatterListCommand { get; }
private IMatterListLoader matterListLoader;
public MainViewModel(IMatterListLoader matterListLoader)
{
this.matterListLoader = matterListLoader;
if (matterListLoader != null && matterListLoader.Properties != null)
{
foreach(var prop in matterListLoader.Properties)
{
if (typeof(prop.Value) == typeof(DateTime))
{
// Draw DateTime picker for datetime value
this.placeHolderPanelForParams.Add(new DateTimePicker() { Name = prop.Key });
}
else
{
// Draw textbox for everything else
this.placeHolderPanelForParams.Add(new TextBox() { Name = prop.Key });
// You can also add validations to the input here (E.g. Dont allow negative numbers of prop is unsigned)
// ...
}
}
}
}
public void LoadFileButtonClick(object sender, EventArgs e)
{
//Get input params from UI
Dictionary<string, object> properties = new Dictionary<string, object>();
foreach(Control propertyControl in this.placeHolderPanelForParams().Children())
{
if (propertyControl is TextBox)
properties.Add(propertyControl.Name, ((TextBox)propertyControl).Text);
else if (propertyControl is DateTimePicker)
properties.Add(propertyControl.Name, ((DateTimePicker)propertyControl).Value);
}
this.matterListLoader.SetProperties(properties);
this.matterListLoader.Load(null); //Ready to load
}
}
不确定为什么没有人建议 属性 属性和反射
只需创建一个新的Attribute
,例如:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ExposeToViewAttribute : Attribute
{
public string Name { get; set; }
public ExposeToViewAttribute([System.Runtime.CompilerServices.CallerMemberName]string name = "")
{
this.Name = name;
}
}
并确保它已添加到您的视图中
var t = matterListLoader.GetType();
var props = t.GetProperties().Where((p) => p.GetCustomAttributes(typeof(ExposeToViewAttribute), false).Any());
foreach(var prop in props)
{
var att = prop.GetCustomAttributes(typeof(ExposeToViewAttribute), true).First() as ExposeToViewAttribute;
//Add to view
}
方法不会变得更干净。
那么用法就很简单了:
[ExposeToView]
public int Something { get; set; }
[ExposeToView("some name")]
public int OtherFieldWithCustomNameThen { get; set; }
但是,如果您使用类似 WPF
的方法,还有其他可能更适合您的解决方案。