可重用的 MVC 代码:处理 linq 表达式中的值类型

Reusable MVC code: handling value types in linq expressions

概览

我正在编写一个 MVC 集,旨在为 Entity Framework 对象提供基本的 search/add/edit/delete 功能。它还做了一些奇特的事情,比如支持使用属性从其他表中填充的下拉菜单。

游戏中的主要对象是一个可继承的通用控制器(AdminController<T>,其中 T 是 EF 对象)包含一个 AdminField<T> 数组,每个元素代表一个 field/property 在 TAdminController<T> 可以生成一个 AdminViewModel<T> 对象。下面是这些对象之间关系的 Visio 图表。

问题

我的问题出在视图中。为了显示值,我使用了 Html.EditorFor/HiddenFor/TextboxFor 助手。一个超级简化的视图简单地循环遍历字段并显示它们的值,如果它们是可编辑字段则允许编辑。这看起来像:

@model AdminViewModel<ExampleEFObject>
@using (Html.BeginForm())
{
    foreach (var f in Model.Fields)
    {
        var expression = Model.ExpressionFromField(f);

        <label class="control-label col-md-3">@f.DisplayName</label>

        @if (!f.Editable)
        {
            @Html.TextBoxFor(expression, new { @class = "form-control", @disabled = "disabled" })
        }
        else
        {
            @Html.EditorFor(expression, new { htmlAttributes = new { @class = "form-control" } })
        }
    }
    <input type="submit" value="Save" class="btn btn-primary" />
}

好吧,这就是我想要它的样子。它与作为引用类型的 fields/properties 一起工作得很好。但是,对于值类型,我在 EditorFor 行收到 System.InvalidOperationException 错误:“Templates can be used only with field access, 属性 access, single-dimensional array索引,或单参数自定义索引器表达式。"

这是因为 ExpressionFromField 方法 return 是一个在 ViewModel 中访问 T 实例的表达式,其 return 类型为 Expression<Func<AdminViewModel<T>, object>>.因为它 return 是一个对象类型,所以在生成表达式时强制装箱,所以我的方法实际上是 returning t => Convert(t.Count) 而不是(例如)t => t.Count。 =33=]

当前解决方法

我目前的解决方法是将第二种方法定义为 Expression<Func<AdminViewModel<T>, int>> IntExpressionFromField(AdminField<T> field)。因为它 return 是一个 int,所以它不会在表达式中强制装箱。然而,这让我的观点变得极其丑陋:

@model AdminViewModel<ExampleEFObject>
@using (Html.BeginForm())
{
    foreach (var f in Model.Fields)
    {
        var expression = Model.ExpressionFromField(f);
        var intExpression = Model.IntExpressionFromField(f);

        <label class="control-label col-md-3">@f.DisplayName</label>

        @if (!f.Editable)
        {
            if (intExpression == null)
            {
                @Html.TextBoxFor(expression, new { @class = "form-control", @disabled = "disabled" })
            }
            else
            {
                @Html.TextBoxFor(intExpression, new { @class = "form-control", @disabled = "disabled" })
            }
        }
        else
        {

            if (intExpression == null)
            {
                @Html.EditorFor(expression, new { htmlAttributes = new { @class = "form-control" } })
            }
            else
            {
                @Html.EditorFor(intExpression, new { htmlAttributes = new { @class = "form-control" } })
            }
        }
    }
    <input type="submit" value="Save" class="btn btn-primary" />
}

更糟糕的是,这只支持 1 种值类型 (int)。我还想支持 decimal、long 和其他任何东西——最好不必为每个创建自定义方法以及所有逻辑来防止其他类型。

求助!

我很想找到一种优雅、简单的方法来解决这个问题。如果 none 存在,我想我的下一步是停止使用 Html 辅助方法;但是,这需要打破与应用程序其余部分的一致性,我什至不知道它能解决问题。

如有任何帮助,我们将不胜感激!

无法使用我现有的方法解决此问题,我尝试了最后提到的方法:放弃 HtmlHelper 方法。这很有效,我希望我很久以前就这样做了。

MVC ViewModel 绑定是使用 "name" 属性 完成的,因此要手动绑定到我的模型,我需要获取不合格的 属性 名称(即表示为 cust => cust.Records.Address.State,我需要字符串 SelectedItem.Records.Address.State(其中 SelectedItem 是包含显示值的 ViewModel 对象)。我的 AdminField<T> 对象已经有一个 MemberExpression 属性,所以我只是使用了 ToString() 方法,删除了参数,并在 ViewModel 对象名称前添加了前缀。丑陋,我知道,但可靠且简单。

之前,我需要表达式来提供 HtmlHelper 对象,但现在我只需要值,所以我用更简单的 GetValue() 方法替换了我的 ExpressionFromField() 方法 returns string 值。重要的是 returns a string;此转换允许我读取和写入所有值类型(因为 ViewModel 绑定处理将编辑后的值转换回原始 属性 类型)。

我的新视图是这样的:

@model AdminViewModel<ExampleEFObject>
@using (Html.BeginForm())
{
    foreach (var f in Model.Fields)
    {
        var propertyName = f.PropertyName(f); //unqualified, can contain multiple parts
        var value = Model.GetValue(f);        //uses the field expression to retrieve the value
                                              //of a "T SelectedItem" object in the ViewModel
        <label class="control-label col-md-3">@f.DisplayName</label>

        @if (!f.Editable)
        {
            <input class="form-control" disabled="disabled" name="@propertyName" type="text" value="@value" />
        }
        else
        {
            <input class="form-control" name="@propertyName" type="text" value="@value" />
        }
    }
    <input type="submit" value="Save" class="btn btn-primary" />
}

干净多了!

我用这种方法失去的一件事是验证。从这里,我可以添加 javascript 验证或创建模板来复制 HtmlHelper 自动添加的数据验证。不管怎样,没什么大不了的。

希望这对某人有所帮助。

注意:@Ivan Stoeve 推荐使用 Html.Editor/Html.Textbox 等,它们通过 属性 名称而不是表达式接受绑定。这仍然让我没有视图中的强类型表达式,但它给了我返回验证,所以我认为值得调整它。它还将清除我在添加下拉选项时必须做的一些丑陋的字符串操作。 :)