Что, если при использовании абстракции и внедрения зависимостей необходимо настраивать детали, специфичные для реализации, в пользовательском интерфейсе?

У меня есть приложение, которое загружает список номеров клиентов / вопросов из входного файла и отображает их в пользовательском интерфейсе. Эти числа представляют собой простые числовые строки с нулями, например «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. Как мне позволить модели представления выполнять свою работу по управлению проблемами представления, но при этом оставить некоторые детали реализации неизвестными ей?


Вот диаграмма зависимостей:

Что, если при использовании абстракции и внедрения зависимостей необходимо настраивать детали, специфичные для реализации, в пользовательском интерфейсе?

Представлению не нужно знать ничего, чего не хочет знать модель представления. Модель представления будет выставлять поведение, с которым она хочет, чтобы представление взаимодействовало, и передает необходимую информацию своим (этой модели представления) зависимостям.

Nkosi 20.09.2018 01:52

Даже модель представления не знает деталей реализации. В этом вся моя точка зрения.

rory.ap 20.09.2018 13:14

@ rory.ap Я думаю, вы путаете между презентацией и реализацией. Если ваш актер (пользователь) не заботится о деталях начальной строки / столбца, то это не касается вашей презентации. Однако в вашем случае кажется, что ваш актер заинтересован в деталях начальной строки / столбца, что также делает его проблемой презентации, хотя это может показаться только проблемой реализации. Если ваш актер не интересуется деталями начальной строки / столбца, то для меня имеет смысл удалить if из vm.

Dipen Shah 20.09.2018 22:52

Зачем вам нужны StartRowNum и StartColNum в виртуальной машине? Возможно, если бы мы поняли причину этого, у нас был бы путь вперед.

Lynn Crumbling 20.09.2018 23:06

Это полностью выдуманный пример. Номер начальной строки и номер начального столбца являются примерами конфигураций, которые может сделать пользователь, но только потому, что в конкретной реализации IMatterListLoader есть такие понятия, как «строки» и «столбцы». В другой реализации этого может не быть; это может быть, например, текстовый файл ASCII со списком, разделенным запятыми, в одной строке. В таком случае нет понятия столбцов и строк, поэтому конфигурация, доступная пользователю, будет совершенно другой. Однако виртуальная машина не должна (не должна) меняться вообще.

rory.ap 21.09.2018 13:15

Как вы ожидаете манипулировать разными данными с помощью одной и той же ViewModel? Если вы имеете дело с разными формами данных, вам нужны разные ViewModels. Если ваш пользователь заинтересован в манипулировании определенными данными, они должны быть представлены через пользовательский интерфейс, что повлечет за собой раскрытие их из ViewModel. Это подразумевает: если вы открываете и манипулируете разными данными, вам вполне могут понадобиться как разные ViewModels, так и разные View, или вам понадобится View, который будет отображаться по-разному в зависимости от формы данных ViewModel ... много, если if (this) show / спрятаться в вашем поле зрения

Bill Richards 21.09.2018 19:14

и это станет беспорядочным и громоздким; то, что я настоятельно не рекомендую. Почему бы не иметь отношения VIew ViewModel 1: 1, это проще поддерживать и понимать

Bill Richards 21.09.2018 19:15

Хорошо, я внес несколько изменений в свой вопрос, которые, надеюсь, внесут в него ясность. Пожалуйста, проверьте это еще раз.

rory.ap 21.09.2018 19:56

Также посмотрите мой обновленный ответ.

Sal 24.09.2018 17:43
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
8
9
230
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

У вас может быть функция, которая создает элементы пользовательского интерфейса на основе конкретного типа интерфейса.

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 для автоматической загрузки правильного представления для типа.

Bradley Uffner 25.09.2018 21:34

Мне очень нравится это решение, спасибо. Теперь я понимаю, что детали реализации загрузчика, по крайней мере, частично важны для логики представления, и теперь мне это удобнее. Ваше решение элегантно инкапсулирует и разделяет проблемы логики представления конкретного загрузчика.

rory.ap 26.09.2018 17:31

Я бы добавил метод 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 21.09.2018 19:58

@ rory.ap Вы правы, я скорректировал свой ответ для лучшего оформления.

Sal 24.09.2018 17:41

Не уверен, почему никто не предлагал атрибуты свойств и отражение

Просто создайте новый 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/…

Aidan Connelly 26.09.2018 10:18

Отражение - такой же инструмент, как и многие другие. Здесь это еще менее «враждебно», чем то, что описано там, и прекрасный пример, в котором можно использовать атрибуты, чтобы упростить кодирование.

X39 26.09.2018 10:56

Отражение не такой инструмент, как любой другой - другие инструменты не нарушают инкапсуляцию так, как это делает отражение.

Aidan Connelly 26.09.2018 11:22

Ну и что? этот случай даже описан как «когда использовать» в вашей связанной статье Dependency injection frameworks ... теперь угадайте, что это делает? он считывает атрибуты общедоступных свойств и использует их. Это так же «враждебно» и «плохо», как и XmlElementAttribute & co. Тот факт, что вас когда-то научили «избегать рефлексии», не означает, что вы никогда не должны использовать языковые методы (а атрибуты ЯВЛЯЮТСЯ языковыми методами!). Отличие от фреймворка, такого как public IEnumerable<Property> Properties, заключается в том, что он не такой агрессивный и сохраняет код чистым.

X39 26.09.2018 12:51

Я "узнал", что использование отражения, когда есть альтернативы, вызывает проблемы, хотя некоторые все еще предпочитают его, это не то место, где я бы его использовал.

Aidan Connelly 26.09.2018 14:23

Другие вопросы по теме