Reusable MVC code: handling value types in linq expressions

别说谁变了你拦得住时间么 提交于 2020-01-15 04:24:48

问题


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.

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!


回答1:


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. :)



来源:https://stackoverflow.com/questions/38081579/reusable-mvc-code-handling-value-types-in-linq-expressions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!