可重复使用的MVC代码:处理linq表达式中的值类型[英] Reusable MVC code: handling value types in linq expressions

本文是小编为大家收集整理的关于可重复使用的MVC代码:处理linq表达式中的值类型的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

概述

我正在编写一个 MVC 集,旨在为实体框架对象提供基本的搜索/添加/编辑/删除功能.它还做了一些花哨的事情,比如支持使用属性从其他表填充的下拉列表.

游戏中的主要对象是一个可继承的通用控制器(AdminController<T>,其中 T 是 EF 对象),其中包含一个 AdminField<T> 数组,其中每个元素代表 T 中的一个字段/属性.AdminController<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" />
}

嗯,这就是我想要它的样子.它适用于作为引用类型的字段/属性.但是,对于值类型,我在 EditorFor 行收到 System.InvalidOperationException 错误:"模板只能用于字段访问、属性访问、单维数组索引或单参数自定义索引器表达式."

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

当前解决方法

我目前的解决方法是将第二种方法定义为 Expression<Func<AdminViewModel<T>, int>> IntExpressionFromField(AdminField<T> field).因为它返回一个 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).我还想支持十进制、长整数和其他任何东西——最好不必为每个方法创建自定义方法以及所有逻辑来防止其他类型.

帮助!

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

任何帮助将不胜感激!

推荐答案

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

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

以前,我需要表达式来提供 HtmlHelper 对象,但现在我只需要值,所以我用返回 string 值的更简单的 GetValue() 方法替换了我的 ExpressionFromField() 方法.重要的是它返回一个 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 等接受绑定通过属性名称而不是表达式.这仍然让我在视图中没有强类型表达式,但它给了我回验证,所以我认为值得调整它.它还将清除我在添加下拉选项时必须做的一些丑陋的字符串操作.:)

本文地址:https://www.itbaoku.cn/post/1556743.html

问题描述

Overview

I am writing an MVC set that is intended to provide basic search/add/edit/delete functionality for an Entity Framework object. It also does some fancy things like supporting dropdowns filled from other tables using attributes.

The main object in play is an inheritable generic controller (AdminController<T> where T is the EF object) containing an array of AdminField<T>, each element of which represents a field/property in T. AdminController<T> can generate an AdminViewModel<T> object. Below is a Visio chart of the relationships between those objects.

enter image description here

The Problem

My problem is in the Views. To display values, I'm using the Html.EditorFor/HiddenFor/TextboxFor helpers. A hyper-simplified view simply loops through the field and displays their values, allowing edits if they're editable fields. This looks like:

@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" />
}

Well, that's what I want it to look like. It works just fine with fields/properties that are reference types. With value types, however, I get a System.InvalidOperationException error on the EditorFor line: "Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions."

This is because the ExpressionFromField method, which returns an expression accessing an instance of T within the ViewModel, has a return type of Expression<Func<AdminViewModel<T>, object>>. Because it returns an object type, boxing is forced when generating the expression so that instead of (for example) t => t.Count, my method is actually returning t => Convert(t.Count).

Current Workaround

My current workaround is to have a second method defined as Expression<Func<AdminViewModel<T>, int>> IntExpressionFromField(AdminField<T> field). Because it returns an int, it doesn't force boxing in the expression. However, it makes my views extremely ugly:

@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" />
}

Worse, this only supports 1 value type (int). I'd also like to support decimal, long, and whatever else--preferably without having to create custom methods for each as well as all the logic to safeguard against other types.

Help!

I'd love to find an elegant, easy way to resolve this issue. If none exists, I think my next step would be to stop using the Html helper methods; however, this would require breaking consistency with the rest of the application and I don't even know that it would solve the issue.

Any help would be much appreciated!

推荐答案

Unable to fix this issue using my existing approach, I tried the approach I mentioned at the end: abandoning HtmlHelper methods. This worked great and I wish I'd done it a long time ago.

MVC ViewModel binding is accomplished using the "name" property, so to manually bind to my model I needed to get the unqualified property name (i.e. for the field expressed as cust => cust.Records.Address.State, I needed the string SelectedItem.Records.Address.State (where SelectedItem is the ViewModel object that contains the displayed value). My AdminField<T> object already had a MemberExpression property, so I just used the ToString() method, removed the parameter, and prepended the ViewModel object name. Ugly, I know, but reliable and easy.

Before, I needed the expression to feed the HtmlHelper object, but now I just needed the value, so I replaced my ExpressionFromField() method with a simpler GetValue() method which returns the string value. It's important that it returns a string; this conversion allows me to read and write all value types (because the ViewModel binding handles the conversion of edited values back to the original property type).

My new views look like this:

@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" />
}

Much cleaner!

One thing I lose with this approach is validation. From here, I can add javascript validation or create templates to duplicate the data validation that HtmlHelper automatically adds. Either way, no big deal.

Hope this helps someone.

Note: @Ivan Stoeve recommended using Html.Editor/Html.Textbox etc., which accept a binding via the property name rather than an expression. This still leaves me without the strongly typed expressions in the view, but it gives me back validation so I think it will be worth adjusting it. It will also clear up some ugly string manipulation I had to do when adding dropdown options. :)