ASP.NET MVC - Problem with EditorTemplate for ICol

2019-01-25 17:18发布

问题:

I have an ASP.NET MVC 3 (Razor) website, and a (simplified) model called Review:

public class Review
{
   public int ReviewId { get; set; }
   public bool RecommendationOne
   {
       // hook property - gets/set values in the ICollection
   }
   public bool RecommendationTwo { // etc }
   public ICollection<Recommendation> Recommendations { get; set; }
}

Recommendation is as follows:

public class Recommendation
{
   public byte RecommendationTypeId
}

I also have an enum called RecommendationType, which i use to map the above recommendation to. (based on RecommendationTypeId).

So to summarize - a single Review has many Recommendations, and each of those Recommendations map to a particular enum type, i expose hook properties to simplify model-binding/code.

So, onto the View:

@Html.EditorFor(model => model.Recommendations, "Recommendations")

Pretty simple.

Now, for the editor template, i want to display a checkbox for each possible RecommendationType (enum), and if the model has that recommendation (e.g on edit view), i check the box.

Here's what i have:

@model IEnumerable<xxxx.DomainModel.Core.Posts.Recommendation>
@using xxxx.DomainModel.Core.Posts;

@{
    Layout = null;
}

<table>
    @foreach (var rec in Enum.GetValues(typeof(RecommendationType)).Cast<RecommendationType>())
    {
        <tr>
            <td>
                @* If review contains this recommendation, check the box *@
                @if (Model != null && Model.Any(x => x.RecommendationTypeId == (byte)rec))
                {
                    @* How do i create a (checked) checkbox here? *@
                }
                else
                {
                    @* How do i created a checkbox here? *@
                }

                @rec.ToDescription()
            </td>
        </tr>
    }
</table>

As the comments suggest - i don't know how to use @Html.CheckBoxFor. Usually that takes an expression based on the model, but i'm how sure how to bind to the hook property based on the currently looped enum value. E.g it needs to dynamically do @Html.CheckBoxFor(x => x.RecommendationOne), @Html.CheckBoxFor(x => x.RecommendationTwo), etc.

The current solution i have (which works), involves manually constructing the <input> (including hidden fields).

But as i'm just getting the hang of editor templates, hoping someone with experience can point me in a "strongly-typed" direction.

Or is there a nicer way (HTML Helper) i can do this?

回答1:

I would start by introducing a proper view model for the scenario:

public enum RecommendationType { One, Two, Three }

public class ReviewViewModel
{
    public IEnumerable<RecommendationViewModel> Recommendations { get; set; }
}

public class RecommendationViewModel
{
    public RecommendationType RecommendationType { get; set; }
    public bool IsChecked { get; set; }
}

Then the controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        // TODO: query the repository to fetch your model
        // and use AutoMapper to map between it and the 
        // corresponding view model so that you have a true/false
        // for each enum value
        var model = new ReviewViewModel
        {
            Recommendations = new[]
            {
                new RecommendationViewModel { 
                    RecommendationType = RecommendationType.One, 
                    IsChecked = false 
                },
                new RecommendationViewModel { 
                    RecommendationType = RecommendationType.Two, 
                    IsChecked = true 
                },
                new RecommendationViewModel { 
                    RecommendationType = RecommendationType.Three, 
                    IsChecked = true 
                },
            }
        };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(ReviewViewModel model)
    {
        // Here you will get for each enum value the corresponding
        // checked value
        // TODO: Use AutoMapper to map back to your model and persist
        // using a repository
        return RedirectToAction("Success");
    }
}

and the corresponding view (~/Views/Home/Index.cshtml):

@model YourAppName.Models.ReviewViewModel

@{
    ViewBag.Title = "Index";
}

@using (Html.BeginForm())
{
    @Html.EditorFor(model => model.Recommendations)
    <input type="submit" value="Go" />
}

and finally the editor template (~/Views/Home/EditorTemplates/RecommendationViewModel.cshtml)

@model YourAppName.Models.RecommendationViewModel
<div>
    @Html.HiddenFor(x => x.RecommendationType)
    @Model.RecommendationType 
    @Html.CheckBoxFor(x => x.IsChecked)
</div>

Now the view code is cleaned as it should. No ifs, no loops, no LINQ, no reflection, this is the responsibility of the controller/mapper layer. So every time you find yourself writing some advanced C# logic in your view I would recommend you rethinking your view models and adapt them as necessary. That's what view models are intended for: to be as close as possible to the view logic.