У меня есть приложение, которое загружает список номеров клиентов / вопросов из входного файла и отображает их в пользовательском интерфейсе. Эти числа представляют собой простые числовые строки с нулями, например «02240/00106». Вот класс ClientMatter:
public class ClientMatter
{
public string ClientNumber { get; set; }
public string MatterNumber { get; set; }
}
Я использую MVVM, и он использует внедрение зависимостей с корнем композиции, содержащимся в пользовательском интерфейсе. Существует служебный интерфейс 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. Как мне позволить модели представления выполнять свою работу по управлению проблемами представления, но при этом оставить некоторые детали реализации неизвестными ей?
Вот диаграмма зависимостей:
Даже модель представления не знает деталей реализации. В этом вся моя точка зрения.
@ rory.ap Я думаю, вы путаете между презентацией и реализацией. Если ваш актер (пользователь) не заботится о деталях начальной строки / столбца, то это не касается вашей презентации. Однако в вашем случае кажется, что ваш актер заинтересован в деталях начальной строки / столбца, что также делает его проблемой презентации, хотя это может показаться только проблемой реализации. Если ваш актер не интересуется деталями начальной строки / столбца, то для меня имеет смысл удалить if из vm.
Зачем вам нужны StartRowNum и StartColNum в виртуальной машине? Возможно, если бы мы поняли причину этого, у нас был бы путь вперед.
Это полностью выдуманный пример. Номер начальной строки и номер начального столбца являются примерами конфигураций, которые может сделать пользователь, но только потому, что в конкретной реализации IMatterListLoader есть такие понятия, как «строки» и «столбцы». В другой реализации этого может не быть; это может быть, например, текстовый файл ASCII со списком, разделенным запятыми, в одной строке. В таком случае нет понятия столбцов и строк, поэтому конфигурация, доступная пользователю, будет совершенно другой. Однако виртуальная машина не должна (не должна) меняться вообще.
Как вы ожидаете манипулировать разными данными с помощью одной и той же ViewModel? Если вы имеете дело с разными формами данных, вам нужны разные ViewModels. Если ваш пользователь заинтересован в манипулировании определенными данными, они должны быть представлены через пользовательский интерфейс, что повлечет за собой раскрытие их из ViewModel. Это подразумевает: если вы открываете и манипулируете разными данными, вам вполне могут понадобиться как разные ViewModels, так и разные View, или вам понадобится View, который будет отображаться по-разному в зависимости от формы данных ViewModel ... много, если if (this) show / спрятаться в вашем поле зрения
и это станет беспорядочным и громоздким; то, что я настоятельно не рекомендую. Почему бы не иметь отношения VIew ViewModel 1: 1, это проще поддерживать и понимать
Хорошо, я внес несколько изменений в свой вопрос, которые, надеюсь, внесут в него ясность. Пожалуйста, проверьте это еще раз.
Также посмотрите мой обновленный ответ.





У вас может быть функция, которая создает элементы пользовательского интерфейса на основе конкретного типа интерфейса.
public static void ConstructUI(IMatterListLoader loader) {
Type loaderType = loader.GetType();
// Do logic based on type
}
У вас могут быть классы для каждой из реализаций IMatterListLoader, которые содержат логику, касающуюся представления. (Вы не хотите смешивать логику представления пользовательского интерфейса с реализациями IMatterListLoader).
В зависимости от типа загрузчика вы используете правильный класс для создания элементов пользовательского интерфейса.
Вам понадобится отдельная модель просмотра для каждого типа файла, который вы собираетесь загрузить.
Каждая модель просмотра выполняет настройку для своего конкретного загрузчика.
Эти модели просмотра затем могут быть переданы как зависимости основной модели просмотра, которая при необходимости вызывает load для каждой модели просмотра;
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)
}
}
}
По мере добавления дополнительных типов в приложение вы будете создавать новые модели представлений и добавлять их в качестве зависимостей.
Это действительно проявляется, когда вы используете неявный шаблон DataTemplate в XAML для автоматической загрузки правильного представления для типа.
Мне очень нравится это решение, спасибо. Теперь я понимаю, что детали реализации загрузчика, по крайней мере, частично важны для логики представления, и теперь мне это удобнее. Ваше решение элегантно инкапсулирует и разделяет проблемы логики представления конкретного загрузчика.
Я бы добавил метод Draw() в интерфейс IMatterListLoader. Затем ваша MainViewModel просто вызовет Draw(), а фактический IMatterListLoader добавит все необходимые параметры в пользовательский интерфейс.
Это немного концептуально, поскольку я не слишком знаком с 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 прав, уровень сервиса не должен знать о компонентах пользовательского интерфейса. Вот мой скорректированный ответ, в котором IMatterListLoader просто предоставляет необходимые свойства, используя словарь в качестве PropertyBag вместо того, чтобы указывать пользовательскому интерфейсу, что рисовать. Таким образом, слой пользовательского интерфейса выполняет всю работу пользовательского интерфейса:
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
}
}
Но IMatterListLoader не имеет понятия «Draw», потому что это класс обслуживания, а не класс представления.
@ rory.ap Вы правы, я скорректировал свой ответ для лучшего оформления.
Не уверен, почему никто не предлагал атрибуты свойств и отражение
Просто создайте новый 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, есть и другие решения, которые могут вам подойти.
Всегда лучше избегать размышлений, где это возможно, по причинам, изложенным в этой относительно короткой статье: brooknovak.wordpress.com/2013/09/24/…
Отражение - такой же инструмент, как и многие другие. Здесь это еще менее «враждебно», чем то, что описано там, и прекрасный пример, в котором можно использовать атрибуты, чтобы упростить кодирование.
Отражение не такой инструмент, как любой другой - другие инструменты не нарушают инкапсуляцию так, как это делает отражение.
Ну и что? этот случай даже описан как «когда использовать» в вашей связанной статье Dependency injection frameworks ... теперь угадайте, что это делает? он считывает атрибуты общедоступных свойств и использует их. Это так же «враждебно» и «плохо», как и XmlElementAttribute & co. Тот факт, что вас когда-то научили «избегать рефлексии», не означает, что вы никогда не должны использовать языковые методы (а атрибуты ЯВЛЯЮТСЯ языковыми методами!). Отличие от фреймворка, такого как public IEnumerable<Property> Properties, заключается в том, что он не такой агрессивный и сохраняет код чистым.
Я "узнал", что использование отражения, когда есть альтернативы, вызывает проблемы, хотя некоторые все еще предпочитают его, это не то место, где я бы его использовал.
Представлению не нужно знать ничего, чего не хочет знать модель представления. Модель представления будет выставлять поведение, с которым она хочет, чтобы представление взаимодействовало, и передает необходимую информацию своим (этой модели представления) зависимостям.