设计指南——从数据库中的松散类型键/值动态视图模型

时间:2022-08-01 12:54:06

I have a scenario that is giving me a headache, and I thought I was going in the right direction, now I have found another problem. It's complicated so, Let's start at the database:

我有一个让我头疼的场景,我认为我的方向是正确的,现在我发现了另一个问题。这很复杂,我们从数据库开始:

I have a table with a key, and a value, a typeId and a few other properties. All I'm concerned with are the key and value essentially. My viewmodel contains a list of the Domain Object I mapped from the Data Object nhibernate uses in the repository layer. Here it is:

我有一个包含键、值、类型id和其他一些属性的表。我关心的是关键和价值。我的viewmodel包含我从存储库层中使用的数据对象nhibernate映射的域对象的列表。这里是:

public class DomainModel
{
    public string Key { get; set; }
    public string Value { get; set; }
    public TypeEnum Type { get; set; }
}

**Note, the TypeEnum has nothing to do with the strong type of the Value for the Key. It is just a different way of categorizing the Keys/Values so that I can pull them from the database by that type.

**注意,类型枚举与键的强类型无关。这是一种对键/值进行分类的不同方法,这样我就可以根据类型从数据库中提取它们。

Simple. Here is my viewmodel:

简单。这是我的视图模型:

public class ViewModel
{
    public List<DomainModel> Models { get; set; }
}

The problem I have is that the values for the keys are different data types. Sometimes they are booleans and I want an Html.CheckBoxFor(model => model.Value), and sometimes they are strings and an Html.TextBoxFor(model => model.Value) will suffice.

我的问题是键的值是不同的数据类型。有时他们是布尔人,我想要一个Html。CheckBoxFor(model => model. value),有时它们是字符串和Html。TextBoxFor(model => model. value)就足够了。

Here is my razor view:

以下是我对剃刀的看法:

@foreach (var setting in Model.Models)
{
    <tr>
        <td>@Html.Label(setting.Key)</td>
        <td>@Html.TextBox(setting.Value)</td>
    </tr>
}

What is the best area of the application to slice here, and do some type inspection or something so that I have the appropriate Html Elements on the page? How would I even go about that? Am I missing something that is really obvious and simple here? Additionally, how do I go about getting a Display Name Attribute for the Keys, based on the Key's value? They are just PacalCasedBunchedDescriptionNames at the moment. Am I just way off on design here or what?

应用程序的最佳区域是什么,然后进行一些类型检查或其他检查,以便在页面上有合适的Html元素?我该怎么做呢?我是不是漏掉了一些明显而简单的东西?此外,如何根据键的值获取键的显示名称属性?它们只是目前的PacalCasedBunchedDescriptionNames。我在设计上是不是走得太远了?

2 个解决方案

#1


1  

What I ended up doing was just leaving the view and viewmodel flat, rather than trying to do some dynamic type implication there. This also allowed me to keep my validation and other attributes nice and neat.

我所做的只是让视图和viewmodel平,而不是尝试在那里做一些动态类型的暗示。这也使我能够保持验证和其他属性的整洁。

Domain Model is still the same, I actually ended up removing Type, and creating a SaveDomainModel in my Business Layer since every time I save / update a collection, it is only for 1 type of Setting:

域模型仍然是相同的,实际上我最终删除了类型,并且在我的业务层中创建了一个SaveDomainModel,因为每次我保存/更新一个集合时,它只针对1种设置:

public class SaveDomainModel
{
    public List<DomainModel> DomainModels { get; set; }
    public SettingTypeEnum SettingType { get; set; }
}

I changed my DomainModel to:

我将我的域名模型改为:

public class DomainModel
{
    [Required]
    public string Key { get; set; }
    [Required]
    public string Value { get; set; }
}

And flattened out my ViewModel:

把我的视图模型展平:

public class EditViewModel
{
    [DisplayName("Display Name:")]
    [Required]
    public int AnIntProp { get; set; }

    [DisplayName("Another Display Name:")]
    [Required]
    public string HereIsAString { get; set; }

    [DisplayName("Bool Display:")]
    [Required]
    public bool ImABool{ get; set; }
}

So now my controller looks like this for the POST:

现在我的控制器看起来是这样的

[HttpPost]
public virtual ActionResult Edit(EditViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        SaveSettings(viewModel);
        return RedirectToAction(MVC.Settings.Edit());
    }
    return View(viewModel);
}

private void SaveSettings(EditViewModel viewModel)
{
    var settings = MapEditViewModelToDomainModels(viewModel);
    var saveDomainModel = new SaveDomainModel
    {
        DomainModels = settings,
        SettingType = SettingTypeEnum.Application
    };
    _settingsService.SaveSettings(saveDomainModel);
}

This is really the missing link that I had not came upon before which I stumbled upon in this post: Enumerating through an object's properties (string) in C#

这正是我在本文中偶然发现的缺失的链接:在c#中枚举对象的属性(string)

Then to map from flat view model to Domain obj I used that Map... function in SaveSettings().

然后从平面视图模型映射到领域obj,我用了这个映射…在储存设定()函数。

private static List<DomainModel> MapEditViewModelToDomainModels(EditViewModel viewModel)
{
    var settings = new List<DomainModel>();

    var stringPropertyNamesAndValues = viewModel.GetType().GetProperties().Where(p => p.CanRead).Select(p => new {Name = p.Name, Value = p.GetValue(viewModel, null)});
    foreach (var pair in stringPropertyNamesAndValues)
    {
        var model= new DomainModel
        {
            Key = pair.Name,
            Value = pair.Value.ToString()
        };
        settings.Add(model);
    }

    return settings;
}

So then I was able to keep my view flat like so:

这样我就能让我的视图保持平坦:

<tr>
    <td>@Html.LabelFor(model => model.SomeString)</td>
    <td>@Html.TextBoxFor(model => model.SomeString)</td>
</tr>
<tr>
    <td>@Html.LabelFor(model => model.SomeBoolean)</td>
    <td>@Html.CheckBoxFor(model => model.SomeBoolean)</td>
</tr>
...>

Then to finish it off I added an UpdateCollection() to my Repository, which is called in the service layer, after mapping from DomainObj -> DataObj, obviously.

然后,为了完成它,我在存储库中添加了UpdateCollection(),显然是从DomainObj -> DataObj映射之后,在服务层中调用它。

public void SaveSettings(SaveDomainModel model)
{
    var settings = MapDomainModelToList(model).AsQueryable();
    _repository.UpdateCollection(settings);
}
private IEnumerable<DataObj> MapDomainModelToList(SaveDomainModel saveDomainModel)
{
    var settings = new List<Setting>();

    foreach (var domainModel in saveDomainModel.DomainModels)
    {
        var setting = GetSetting(domainModel.Key, saveDomainModel.SettingType);

        if (!String.Equals(setting.Value, domainModel.Value, StringComparison.CurrentCultureIgnoreCase))
        {
            setting.Value = domainModel.Value;
            setting.LastUpdated = DateTime.Now;
            settings.Add(setting);
        }
    }

    return settings;
}
public bool UpdateCollection(IQueryable<T> entities)
{
    using (var transaction = _session.BeginTransaction())
    {
        foreach (var entity in entities)
        {
            _session.Update(entity);
        }

        transaction.Commit();
    }
    return true;
}

#2


0  

One way would be to just use the MVC features to implement this and not touch nhibernate specifics at all. I mean, nhibernate is just your data layer right? This should not influence your presentation layer at all.

一种方法是使用MVC特性来实现这一点,而不涉及nhibernate的细节。我的意思是,nhibernate只是你的数据层,对吧?这一点不应该影响你的表现层。

And you have already a TypeEnum property on your model. I guess this will define if the property should be displayed as checkbox, textbox or whatever... If so, write a custom editortemplate for your DomainModel type and have the logic in one place of how to present an instance of DomainModel.

模型上已经有一个类型枚举属性。我猜这将定义属性是否应该显示为复选框、文本框或其他……如果是这样,那么为您的DomainModel类型编写一个自定义编辑器模板,并将如何显示DomainModel实例的逻辑放在一个位置。

If you are curious about what the editor templates are in MVC, have a look into Scott's blog or this one

如果您对MVC中的编辑器模板感兴趣,请查看Scott的博客或本文

To give you an example of how this could look like:

给大家举个例子

The models:

模型:

public class Entity : List<Property>
{
    public Entity()
    {
    }
}

public class Property
{
    public string Name { get; set; }
    public string Value { get; set; }
    public DisplayType DisplayType { get; set; }
}

public enum DisplayType
{
    TextBox,
    Checkbox
}

A Controller/Action for testing:

一个控制器/动作测试:

    public ActionResult Index()
    {
        var entity = new Entity();
        entity.Add(new Property()
        {
            DisplayType = DisplayType.Checkbox,
            Name = "Check1",
            Value = "True"
        });

        entity.Add(new Property()
        {
            DisplayType = DisplayType.Checkbox,
            Name = "Check2",
            Value = "False"
        });

        entity.Add(new Property()
        {
            DisplayType = DisplayType.TextBox,
            Name = "Input1",
            Value = ""
        });

        //ViewBag.Entity = entity;
        return View(entity);
    }

The View could look like this:

视图可以是这样的:

@using WebApplication6.Models
@model WebApplication6.Models.Entity

@{
    ViewBag.Title = "Edit Entity";
}

<h2>Edit Entity</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Entity</h4>
        <hr />
        @Html.ValidationSummary(true)
        <div class="form-group">
            @for (var i = 0; i < Model.Count;i++ )
            {
                <div class="form-group">
                    @Html.EditorFor(m => m[i], "PropertyEditor")
                </div>
            }
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}

All the magic is now hidden within @Html.EditorFor(m => m[i], "PropertyEditor") Create a folder EditorTemplates under View/Shared and add a file for your template e.g. PropertyEditor.cshtml

所有的魔法现在都隐藏在@Html中。EditorFor(m => m[i],“PropertyEditor”)在View/Shared下创建文件夹编辑器模板,并为模板添加文件,例如PropertyEditor.cshtml

The template could look like this:

模板可以是这样的:

@model WebApplication6.Models.Property

@if (Model != null)
{
    <label for="@Model.Name" class="col-sm-2 control-label">@Model.Name</label>

    switch (Model.DisplayType)
    {
        case WebApplication6.Models.DisplayType.TextBox:
            <div class="col-sm-10">@Html.TextBox(Model.Name, Model.Value)</div>
            break;
        case WebApplication6.Models.DisplayType.Checkbox:
            <div class="col-sm-10">@Html.CheckBox(Model.Name, bool.Parse(Model.Value))</div>
            break;
    }
}

#1


1  

What I ended up doing was just leaving the view and viewmodel flat, rather than trying to do some dynamic type implication there. This also allowed me to keep my validation and other attributes nice and neat.

我所做的只是让视图和viewmodel平,而不是尝试在那里做一些动态类型的暗示。这也使我能够保持验证和其他属性的整洁。

Domain Model is still the same, I actually ended up removing Type, and creating a SaveDomainModel in my Business Layer since every time I save / update a collection, it is only for 1 type of Setting:

域模型仍然是相同的,实际上我最终删除了类型,并且在我的业务层中创建了一个SaveDomainModel,因为每次我保存/更新一个集合时,它只针对1种设置:

public class SaveDomainModel
{
    public List<DomainModel> DomainModels { get; set; }
    public SettingTypeEnum SettingType { get; set; }
}

I changed my DomainModel to:

我将我的域名模型改为:

public class DomainModel
{
    [Required]
    public string Key { get; set; }
    [Required]
    public string Value { get; set; }
}

And flattened out my ViewModel:

把我的视图模型展平:

public class EditViewModel
{
    [DisplayName("Display Name:")]
    [Required]
    public int AnIntProp { get; set; }

    [DisplayName("Another Display Name:")]
    [Required]
    public string HereIsAString { get; set; }

    [DisplayName("Bool Display:")]
    [Required]
    public bool ImABool{ get; set; }
}

So now my controller looks like this for the POST:

现在我的控制器看起来是这样的

[HttpPost]
public virtual ActionResult Edit(EditViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        SaveSettings(viewModel);
        return RedirectToAction(MVC.Settings.Edit());
    }
    return View(viewModel);
}

private void SaveSettings(EditViewModel viewModel)
{
    var settings = MapEditViewModelToDomainModels(viewModel);
    var saveDomainModel = new SaveDomainModel
    {
        DomainModels = settings,
        SettingType = SettingTypeEnum.Application
    };
    _settingsService.SaveSettings(saveDomainModel);
}

This is really the missing link that I had not came upon before which I stumbled upon in this post: Enumerating through an object's properties (string) in C#

这正是我在本文中偶然发现的缺失的链接:在c#中枚举对象的属性(string)

Then to map from flat view model to Domain obj I used that Map... function in SaveSettings().

然后从平面视图模型映射到领域obj,我用了这个映射…在储存设定()函数。

private static List<DomainModel> MapEditViewModelToDomainModels(EditViewModel viewModel)
{
    var settings = new List<DomainModel>();

    var stringPropertyNamesAndValues = viewModel.GetType().GetProperties().Where(p => p.CanRead).Select(p => new {Name = p.Name, Value = p.GetValue(viewModel, null)});
    foreach (var pair in stringPropertyNamesAndValues)
    {
        var model= new DomainModel
        {
            Key = pair.Name,
            Value = pair.Value.ToString()
        };
        settings.Add(model);
    }

    return settings;
}

So then I was able to keep my view flat like so:

这样我就能让我的视图保持平坦:

<tr>
    <td>@Html.LabelFor(model => model.SomeString)</td>
    <td>@Html.TextBoxFor(model => model.SomeString)</td>
</tr>
<tr>
    <td>@Html.LabelFor(model => model.SomeBoolean)</td>
    <td>@Html.CheckBoxFor(model => model.SomeBoolean)</td>
</tr>
...>

Then to finish it off I added an UpdateCollection() to my Repository, which is called in the service layer, after mapping from DomainObj -> DataObj, obviously.

然后,为了完成它,我在存储库中添加了UpdateCollection(),显然是从DomainObj -> DataObj映射之后,在服务层中调用它。

public void SaveSettings(SaveDomainModel model)
{
    var settings = MapDomainModelToList(model).AsQueryable();
    _repository.UpdateCollection(settings);
}
private IEnumerable<DataObj> MapDomainModelToList(SaveDomainModel saveDomainModel)
{
    var settings = new List<Setting>();

    foreach (var domainModel in saveDomainModel.DomainModels)
    {
        var setting = GetSetting(domainModel.Key, saveDomainModel.SettingType);

        if (!String.Equals(setting.Value, domainModel.Value, StringComparison.CurrentCultureIgnoreCase))
        {
            setting.Value = domainModel.Value;
            setting.LastUpdated = DateTime.Now;
            settings.Add(setting);
        }
    }

    return settings;
}
public bool UpdateCollection(IQueryable<T> entities)
{
    using (var transaction = _session.BeginTransaction())
    {
        foreach (var entity in entities)
        {
            _session.Update(entity);
        }

        transaction.Commit();
    }
    return true;
}

#2


0  

One way would be to just use the MVC features to implement this and not touch nhibernate specifics at all. I mean, nhibernate is just your data layer right? This should not influence your presentation layer at all.

一种方法是使用MVC特性来实现这一点,而不涉及nhibernate的细节。我的意思是,nhibernate只是你的数据层,对吧?这一点不应该影响你的表现层。

And you have already a TypeEnum property on your model. I guess this will define if the property should be displayed as checkbox, textbox or whatever... If so, write a custom editortemplate for your DomainModel type and have the logic in one place of how to present an instance of DomainModel.

模型上已经有一个类型枚举属性。我猜这将定义属性是否应该显示为复选框、文本框或其他……如果是这样,那么为您的DomainModel类型编写一个自定义编辑器模板,并将如何显示DomainModel实例的逻辑放在一个位置。

If you are curious about what the editor templates are in MVC, have a look into Scott's blog or this one

如果您对MVC中的编辑器模板感兴趣,请查看Scott的博客或本文

To give you an example of how this could look like:

给大家举个例子

The models:

模型:

public class Entity : List<Property>
{
    public Entity()
    {
    }
}

public class Property
{
    public string Name { get; set; }
    public string Value { get; set; }
    public DisplayType DisplayType { get; set; }
}

public enum DisplayType
{
    TextBox,
    Checkbox
}

A Controller/Action for testing:

一个控制器/动作测试:

    public ActionResult Index()
    {
        var entity = new Entity();
        entity.Add(new Property()
        {
            DisplayType = DisplayType.Checkbox,
            Name = "Check1",
            Value = "True"
        });

        entity.Add(new Property()
        {
            DisplayType = DisplayType.Checkbox,
            Name = "Check2",
            Value = "False"
        });

        entity.Add(new Property()
        {
            DisplayType = DisplayType.TextBox,
            Name = "Input1",
            Value = ""
        });

        //ViewBag.Entity = entity;
        return View(entity);
    }

The View could look like this:

视图可以是这样的:

@using WebApplication6.Models
@model WebApplication6.Models.Entity

@{
    ViewBag.Title = "Edit Entity";
}

<h2>Edit Entity</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Entity</h4>
        <hr />
        @Html.ValidationSummary(true)
        <div class="form-group">
            @for (var i = 0; i < Model.Count;i++ )
            {
                <div class="form-group">
                    @Html.EditorFor(m => m[i], "PropertyEditor")
                </div>
            }
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}

All the magic is now hidden within @Html.EditorFor(m => m[i], "PropertyEditor") Create a folder EditorTemplates under View/Shared and add a file for your template e.g. PropertyEditor.cshtml

所有的魔法现在都隐藏在@Html中。EditorFor(m => m[i],“PropertyEditor”)在View/Shared下创建文件夹编辑器模板,并为模板添加文件,例如PropertyEditor.cshtml

The template could look like this:

模板可以是这样的:

@model WebApplication6.Models.Property

@if (Model != null)
{
    <label for="@Model.Name" class="col-sm-2 control-label">@Model.Name</label>

    switch (Model.DisplayType)
    {
        case WebApplication6.Models.DisplayType.TextBox:
            <div class="col-sm-10">@Html.TextBox(Model.Name, Model.Value)</div>
            break;
        case WebApplication6.Models.DisplayType.Checkbox:
            <div class="col-sm-10">@Html.CheckBox(Model.Name, bool.Parse(Model.Value))</div>
            break;
    }
}