简体   繁体   中英

Using EditorFor in Foreach or For loop (ASP.NET MVC + RAZOR)

I'm currently implementing a Family Tree system in my ASP.NET MVC project. In order to set the relationship between family members, I need to display two ComboBox/DropDownList per row to define relationships from one member to the other.

First I will share my codes and then I will explain what ways I've tried so far and what was the result at the end.

ViewModel

public class FamilyTreeRelationshipViewModel
{

    [ScaffoldColumn(false)]
    public string FromMemberId { get; set; }

    [ScaffoldColumn(false)]
    public string ToMemberId { get; set; }


    public Member FromMember { get; set; }
    public IEnumerable<Member> ToMembers { get; set; }


    [UIHint("FTComboBox")]
    [AdditionalMetadata("BindTo", "relationships")]
    [Required]
    [Display(Name = "From Relationship")]
    public string FromRelationship { get; set; }


    [UIHint("FTComboBox")]
    [AdditionalMetadata("BindTo", "relationships")]
    [Required]
    [Display(Name = "To Relationship")]
    public string ToRelationship { get; set; }
}

Controller

public class FamilyTreeController : Controller
{
    private AppMVC db = new AppMVC();


    public ActionResult Index(Guid? cid, Guid? mid)
    {

        if (cid == null && mid == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.NotFound);
        }


        var frommember = db.Member.FirstOrDefault(x => x.MemberId == mid && x.CaseId == cid && x.Deleted == false);
        var tomembers = db.Member.Where(x => x.CaseId == cid && x.MemberId != mid.Value && x.Deleted == false).ToList();


        ViewBag.cid = cid;
        ViewBag.mid = mid;

        PopulateRelationship();


        var familyTreeRelationshipViewModel = new FamilyTreeRelationshipViewModel
        {
            FromMember = frommember,
            ToMembers = tomembers,
        };

        return View(familyTreeRelationshipViewModel);
    }



    public void PopulateRelationship()
    {
        var relationship = db.RelationshipDD
        .Where(c => c.Deleted == false && c.Code != "PA")
        .OrderBy(c => c.OrderIndex)
        .Select(c => new RelationshipDDViewModel
        {
            Code = c.Code,
            Definition = c.Definition
        })
        .ToList();

        ViewData["relationships"] = relationship;

    }

}

Editor Template for ComboBox

@model object

@{
    var bindto = ViewData.ModelMetadata.AdditionalValues["BindTo"].ToString();
    var fieldname = ViewData.ModelMetadata.PropertyName;
    var prefix = ViewData.TemplateInfo.HtmlFieldPrefix;

}

@(Html.Kendo().ComboBoxFor(m => m)
          .Filter("contains")
          .Placeholder(ViewData.ModelMetadata.DisplayName)
          .DataTextField("Definition")
          .DataValueField("Code")
          .HtmlAttributes(new { style = "width: 100%", Id = prefix})
          .BindTo((System.Collections.IEnumerable)ViewData[bindto])

          )

@Html.ValidationMessageFor(m => m, "", new { @class = "mdc-text-red-400" })

In order to show each row a new result, I used foreach in View:

@if (Model.ToMembers != null)
{
    if (Model.ToMembers.Any())
    {
        foreach (var othermembers in Model.ToMembers.OrderBy(x => x.MemberNumberSuffix))
        {
            @Html.EditorFor(m => m.ToRelationship)
            @Html.EditorFor(m => m.FromRelationship)
        }
    }
}

As you see below in the screenshot, only ComboBoxes in the first row were rendered. I assume it's because of the same control Id for each ComboBox. I checked the browser developer tools (F12), all of them had the same Id. 的ForEach

Later I thought I should use For instead of Foreach and see what happens:

@if (Model.ToMembers != null)
{
    if (Model.ToMembers.Any())
    {
        for (var i = 0; i < Model.ToMembers.Count(); i++)
        {
            var othermembers = Model.ToMembers.ToList()[i];

            @Html.EditorFor(m => m.ToRelationship[i])
            @Html.EditorFor(m => m.FromRelationship[i])
        }
    }
}

As you see below in the screenshot, all of the ComboBoxes are gone and everything has been rendered as Char . The only difference here is that each control/input has it's own Id and that's good but not good as I was expecting. 对于

After all, I decided to use the Built-in DropDownList (MVC) for this purpose and it was the same result. Because I thought something is wrong with Telerik controls.

I even tried to use the ComboBox directly inside the View instead of EditorFor , the result was different. Each row was rendered separately and successfully but it was again Char type and even the error message says that. Normally it should say, "From relationship field is required".

@if (Model.ToMembers != null)
{
    if (Model.ToMembers.Any())
    {
        for (var i = 0; i < Model.ToMembers.Count(); i++)
        {

            @(Html.Kendo().ComboBoxFor(m => m.ToRelationship[i])
            .Filter("contains")
            .Placeholder(ViewData.ModelMetadata.DisplayName)
            .DataTextField("Definition")
            .DataValueField("Code")
            .HtmlAttributes(new { style = "width: 100%" })
            .BindTo((System.Collections.IEnumerable)ViewData["relationships"])
            )

            @(Html.Kendo().ComboBoxFor(m => m.FromRelationship[i])
            .Filter("contains")
            .Placeholder(ViewData.ModelMetadata.DisplayName)
            .DataTextField("Definition")
            .DataValueField("Code")
            .HtmlAttributes(new { style = "width: 100%" })
            .BindTo((System.Collections.IEnumerable)ViewData["relationships"])
            )
        }
    }
}

Screenshot: 直接


Questions

  1. Why can't I use EditorFor for this purpose?
  2. Why the type has changed to Char and how can I fix this?
  3. Any alternative way to achieve this?

Thanks in advance for your help!

  1. You can but you will need to pass the ViewData in to the child template to have access to it in the child view as my understanding is that Viewdata is not a globally available (global to the request anyway) object, it's only valid for the current view (and not any child views unless passed in)

  2. I think the problem here is your view model, you appear to be trying to bind the drop down on all rows to the same property in the root view model, but what I believe you likely more need to do is set relationship values on each Member in the ToMembers property on your root model for example ...

As a seudocode only example it feels like instead of ...

foreach(var child in model.members)
{
    dropdownfor(m => m.FromRelationship, ViewData["relationships"]);
    dropdownfor(m => m.ToRelationship, ViewData["relationships"]);
}

... you should be setting the relationship between 2 people by building dropdowns that set their properties, something like ...

foreach(var child in model.members)
{
    dropdownfor(m => child.FromRelationship, ViewData["relationships"]);
    dropdownfor(m => child.ToRelationship, ViewData["relationships"]);
}

... where the following is true ...

  1. the first param is the property that you are setting to the value of the selected item in the dropdown list
  2. the second param is the list of "relationship types" you appear to be storing in the viewdata object
  3. this exists in the root view not some template / child view

....

  1. How can we achieve the desired result?

Well we can ensure we are setting the right bits of the right things to start off with, I think therefore that what you really want is a root model that looks something like this ...

public class FamilyTreeRelationshipViewModel
{
    public IEnumerable<MemberPair> FamilyMemberPairs{ get; set; }
    public IEnumerable<Relationship> Relationships { get;set; }
}

... from there for each member you can render your rows like this ...

@foreach(var pair in Model.MemberPairs)
{
    @* Template out a member pair, pass through the set of relationships *@
    @Html.EditorFor(m => pair, Relationships)
}

.. ok from here you will need a view in the EditorTemplates folder called "MemberPair.cshtml" which will template a member pair object, that object will loook like this ...

class MemberPair
{
   public Member From { get;set; }
   public Member To { get;set; }
   public int FromRelationshipId {get;set;}
   public int ToRelationshipId {get;set;}
}

... this viewmodel defines a pair of family members that you want to define a relationship between, the from and to family members hould be set and the relationships we will define using the dropdowns we are going to generate in the view like this ...

@Html.DisplayFor(m => m.From) 

@(Html.Kendo().ComboBoxFor(m => m.FromRelationshipId)
            .DataTextField("Definition")
            .DataValueField("Code")
            .DataSource((System.Collections.IEnumerable)ViewData["Relationships"])
            )

@(Html.Kendo().ComboBoxFor(m => m.ToRelationshipId)
            .DataTextField("Definition")
            .DataValueField("Code")
            .DataSource((System.Collections.IEnumerable)ViewData["Relationships"])
            )

@Html.DisplayFor(m => m.To)

In this context the relationship collection is available from viewdata because we explicitly passed it through from the parent in the previous view.

We can also note here that when we bind our dropdowns we are binding in a way that will set a single int relationship id on a single relationship pair object.

Where I believe you went wrong above is that you were trying to bind all of the dropdowns to the same int value on the parent model which I don't know if MVC will allow / cater for as this can cause field id conflicts on the client.

Disclaimer:

This answer describes the theory behind your implementation but is not the exact code you may need, and this will also require some logic change in the controller to manage the construction of the models for these views.

Take your time to think about the structure you need to build and construct the solution with smaller manageable component parts.

This feels like a layered problem rather than something you can achieve in a single view, you could also consider using a kendo grid to do your binding as it would be able to figure this stuff out for you but i didn't want to suggest using a complex control to replace a simple combo binding issue with a complex binding one.

First, to explain your errors. You cannot use a foreach loop to generate form controls for a collection. It generates duplicate name attributes which cannot bind back to a collection (and duplicate id attributes which is invalid html). You need to use a for loop, or better a custom EditorTemplate . You second error (when you do use a for loop) is because the property your binding to is string , so binding to m => m.ToRelationship[i] is binding to a character (typeof Char ) in the string .

The real issue is that your view model is incorrect and has no relationship to what your displaying/editing in the view, and you need to bind the dropdownlists to a property in a collection.

In addition, your use of 2 comboboxes for the ToRelationship and FromRelationship is going to lead to a lot of potential errors, and you should be redefining your Relationship table to include (say) the following fields

ID, Relationship, MaleReverseRelationship, FemaleReverseRelationship

so a typical rows might contain

1, Son, Father, Mother
2, Wife, Husband, NULL
3, Niece, Uncle, Aunt

then you need a single dropdownlist and if the relationship of Jack to Roger is Son , you know that the reverse relationship is Father (assuming your Member model has a field for Gender ). This will greatly simplify your view model, make the UI simpler and prevent user errors if the user selects the wrong reverse relationship.

You have not provided any information on your other tables/models, but you would need a MemberRelationShip table with fields

ID, Member (FK to Members), OtherMember(FK to Members), RelationShip(FK to Relationships).

You then needs 2 view models, one for the parent Member and one to create the relationships

public class ParentMemberVM
{
    public int ID { get; set; }
    public string Name { get; set; }
    .... other properties to display
    public IEnumerable<MemberVM> Members { get; set; }
    public IEnumerable<SelectListItem> RelationshipList { get; set; }
}
public class MemberVM
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Relationship { get; set; }
}

Then you need an EditorTemplate for MemberVM

/Views/Shared/EditorTemplates/MemberVM.cshtml

@model MemberVM
@Html.HiddenFor(m => m.ID)
@Html.DisplayFor(m => m.Name)
@Html.DropDownListFor(m => m.Relationship, (IEnumerable<SelectListItem>)ViewData["relationships"])

and in the main view

@model ParentMemberVM
....
@using (Html.BeginForm())
{
    @Html.HiddenFor(m => m.ID)
    @Html.DislayFor(m => m.Name)
    @Html.EditorFor(m => m.Members, new { relationships = Model.RelationshipList })
    <input type="submit" value="Save" />
}
  1. As an alternate solution you may simply not use HTML helpers and write your own HTML. Using HTML helpers leads to issues such as those you are encountering. You may overcome them, but what is the point using HTML helpers if instead of helping you, they create difficulties? (In my point of view, MVC is about having control over the front code you serve to browsers.)

With Razor, writing directly the required HTML can prove more effective than letting HTML helpers do that for you.

Granted, this is my point of view. I have detailed it a bit more on some similar question here .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM