使用抽象和依赖项注入,如果需要在UI中配置特定于实现的细节该怎么办?

时间:2018-08-29 14:49:40

标签: c# mvvm dependency-injection abstraction separation-of-concerns

我有一个应用程序,该应用程序从输入文件中加载客户编号列表,并将其显示在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实现来加载问题列表,如下所示:

enter image description here

我想允许用户在运行时配置列表开始处的行号和列号,因此视图可能如下所示:

enter image description here

这是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文本文件的实现,我可能会在下一个版本的应用程序中考虑它:

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 :(得分: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之类的东西,那么还有其他解决方案可能会更适合您。