我有一个应用程序,该应用程序从输入文件中加载客户编号列表,并将其显示在UI中。这些数字是简单的零填充数字字符串,例如“ 02240/00106”。这是ClientMatter
类:
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实现所需的行和列号,但是我有一个布尔型标志,指示客户端/事务号是从第一行开始(即没有标题行)还是从第二行开始行(即带有标题行)。
我认为视图模型应该不了解IMatterListLoader
的实现之间的变化。我如何让视图模型完成其控制展示方面的工作,但仍使某些实现细节未知?
这是依赖图:
答案 0 :(得分:4)
对于要加载的每种文件类型,您都需要一个单独的视图模型。
每个viewmodel都会为其特定的加载程序进行设置。
然后可以将这些视图模型作为依赖项传递给主视图模型,后者在需要时调用每个视图模型上的负载;
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)
}
}
}
当您向应用程序中添加更多类型时,您将创建新的视图模型并将其添加为依赖项。
答案 1 :(得分:0)
您可以具有一个根据界面的特定类型构造UI元素的函数。
def _build_hierarchy(*keys):
key_len = len(keys)
result = []
if key_len < 1:
return result
result.append(keys[0])
for i in range(2, key_len + 1):
#the first i-1 should be continuous, the last can be separate.
pre_i = i - 1
count = key_len - pre_i
pre_str = ':'.join(keys[0:pre_i])
for j in range(0, count):
result.append(pre_str + ':' + keys[j + pre_i])
return result
print _build_hierarchy()
print _build_hierarchy('a', 'b', 'c', 'd')
您可以为每个IMatterListLoader实现提供类,其中包含与表示有关的逻辑。 (您不想将UI呈现逻辑与IMatterListLoader实现混在一起。)
根据加载程序的类型,使用正确的类来生成UI元素。
答案 2 :(得分:0)
我将向Draw()
接口添加一个IMatterListLoader
方法。然后,您的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
}
}
答案 3 :(得分:-1)
不确定为什么没人建议属性属性和反射
只需创建一个新的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
之类的东西,那么还有其他解决方案可能会更适合您。