Используя абстракцию и внедрение зависимостей, что, если специфичные для реализации детали должны быть настраиваемыми в пользовательском интерфейсе? - PullRequest
0 голосов
/ 29 августа 2018

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

enter image description here

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

enter image description here

А вот реализация MS Excel IMatterListLoader:

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, которую я мог бы рассмотреть для следующей версии приложения:

enter image description here

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


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

enter image description here

Ответы [ 4 ]

0 голосов
/ 25 сентября 2018

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

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

0 голосов
/ 21 сентября 2018

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

Каждая view-модель выполняет настройку для своего конкретного загрузчика.

Эти видовые модели затем могут быть переданы в качестве зависимостей основной видовой модели, которая при необходимости вызывает 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)
        }
    }
}

По мере добавления новых типов в приложение вы будете создавать новые модели представлений и добавлять их в качестве зависимостей.

0 голосов
/ 21 сентября 2018

Я бы добавил 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
    }
}
0 голосов
/ 29 августа 2018

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

public static void ConstructUI(IMatterListLoader loader) {
    Type loaderType = loader.GetType();
    // Do logic based on type
}

У вас могут быть классы для каждой реализации IMatterListLoader, которая содержит логику, касающуюся представления. (Вы не хотите смешивать логику представления пользовательского интерфейса с реализациями IMatterListLoader).

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

...