Search code examples
c#asp.netasp.net-mvcrazortelerik

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. For

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: For Direct


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!


Solution

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