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.
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; }
}
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;
}
}
@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.
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"])
)
}
}
}
Thanks in advance for your help!
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" />
}