locked
A View that uses multiple models breaks asp-for tag helper RRS feed

  • Question

  • User1434241939 posted

    I have a View that lets users update a 'links' table, linking a person to an object, in this case an air sickness bag (yes, really).  Because of this, I pass the following data model to my view:

    public class LinkViewModel
    {
    public Bagsmvc Bag { get; set; }
    public Linksmvccore Link { get; set; }
    public List<SelectListItem> Options { get; set; } // to generate a drop-down list of people
    }

    All I want to do is match a person (from Options drop down) to a bag and put the results in Link,  Here's the view

    @model BagContext.LinkViewModel

    <form asp-action="Create">
      <div class="form-group">
        <input asp-for="@Model.Link.BagId" value="@Model.Bag.Id" class="form-control" readonly="@(true)" />
        @*<input id="BagId" name="BagId" value="@Model.Bag.Id" class="form-control" readonly="@(true)"/>*@
      </div>
      <div class="form-group">
        <select name="PersonID" asp-items="@Model.Options" onchage=""></select>
      </div>
      <div class="form-group">
        <input type="submit" value="Create" class="btn btn-primary" />
      </div>
    </form>

    And this is how the form is submitted to the controller.

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create([Bind("LinkNumber,BagId,PersonId")] Linksmvccore linksmvccore)

    This does not work because Core tries to match up Link.BagId with BagId and can't do it.  The line I commented out in the View does work, but why use tag helpers at all if I'm just going to hard code 'magical strings' into the code?  In fact, Binding itself uses these magical strings too. 

    What am I missing here that would allow me to use the tag-helpers properly?  How can I get this to work?

    Thursday, May 7, 2020 3:02 PM

Answers

  • User475983607 posted

    When I examine (F12) the HTML the View generates, I see name="Link.BagId" so I guess that won't match up with BagId in the bind statement.  I just don't like hard coding BagId in the view (not to mention the Bind).  As an aside, Bind doesn't even seem to support Intellisense so I can just type any old string in there.

    In my opinion, the hard part is understanding the model binding naming convention.  It seems you get that part which is good.

    If you decided that want to use tag helpers then you need to follow the naming rules.  If you tell the tag helper that the object values belongs to Link.BagId then the model binder looks for Link.BagId not BagId.  If you want the model binder to bind to BagId then move the BagId property to the root of the ViewModel.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Friday, May 8, 2020 5:16 PM

All replies

  • User475983607 posted

    The intent is hard to understand.  As written you are trying to pass the value found in Bag.Id to Link.BagId.  You don't need an input to do this.  Just assign the values when the ViewModel is created.  Still the intent is confusing.

    Thursday, May 7, 2020 3:14 PM
  • User1434241939 posted

    mgebhard

    Just assign the values when the ViewModel is created.

    That's a great idea, I will just do that.  But the question still remains about the PersonID field in the <select>.  I'd really rather use asp-for="@Model.Link.PersonId" or even name="@Model.Link.PersonId".  But unfortunately, the controller will not bind these to the correct field.  But there should be a way to do so, unless this entire framework only works for the most basic of CRUD operations.

    Thursday, May 7, 2020 3:22 PM
  • User475983607 posted

    stevebo

    That's a great idea, I will just do that.  But the question still remains about the PersonID field in the <select>.  I'd really rather use asp-for="@Model.Link.PersonId" or even name="@Model.Link.PersonId".  But unfortunately, the controller will not bind these to the correct field.  But there should be a way to do so, unless this entire framework only works for the most basic of CRUD operations.

    Your confusing general HTML Form design, the UI, with the data access layer.   Design the form to have all the elements that the user needs to make a selection.   You have not provide the data models so at this point the design is a bit confusing.  

    Is the Link table a many-to-many relationship between Bags and People?  The Link table has a BagId and a PersonId as the primary key?

    Thursday, May 7, 2020 3:43 PM
  • User1434241939 posted

    Yes, the Link table is many-to-many:

    namespace AirSicknessBags.Models
    {
    [Table("linksmvccore")]
    public partial class Linksmvccore
    {
    [Key]
    [Column(TypeName = "int(11)")]
    public int LinkNumber { get; set; }
    [Column("BagID", TypeName = "int(11)")]
    public int? BagId { get; set; }
    [Column("PersonID", TypeName = "int(11)")]
    public int? PersonId { get; set; }
    }
    }

    Here is the Bag table (I omitted several fields for this example):

    namespace AirSicknessBags.Models
    {
        [Table("bagsmvc")]
        public partial class Bagsmvc
        {
            [Key]
            [Column("id", TypeName = "int(11)")]
            public int Id { get; set; }
            [Column(TypeName = "varchar(255)")]
            [Required]
            public string Airline { get; set; } = "No Airline";
            [Column(TypeName = "longtext")]
            [Display(Name = "Pithy Description")]
            public string Detail { get; set; }
        }
    }

    And here is the People table (minus extra fields):

    namespace AirSicknessBags.Models
    {
    [Table("peoplemvc")]
    public partial class Peoplemvc
    {
    [Key]
    [Column(TypeName = "int(11)")]
    public int PersonNumber { get; set; }
    [Column(TypeName = "varchar(255)")]
    [Display(Name = "First Name")]
    public string FirstName { get; set; }
    [Column(TypeName = "varchar(255)")]
    public string LastName { get; set; }
    [Column(TypeName = "varchar(255)")]
    }
    }

    So when someone looks at a bag in the bag's Display View, there's a button that says "Link to Person".  Clicking that button fires the Create action in the Links controller.  As you suggested earlier, I set the Links.BagId = Bags.Id.  Then I return the Create View for Links, passing it a model for Links and People, since I want to choose one of the People to link in the Create View.  For example Links.PersonId = People.PersonNumber.  (I like to name join fields differently so that there's no ambiguity as to which field I'm referring to, even if it's not the best practice).

    I know that I'm not very good with all this yet, but maybe you know what information I'm missing.  Thank you for all your time.

    Thursday, May 7, 2020 6:30 PM
  • User-474980206 posted

    your form has form 2 fields. (a input and a select>. as the submit does not have a name its not included

      Link.BagId=<input text box value>&PersonID=<select value>

    in your controller you are trying to bind to a Linksmvccore. but to bind to a model it needs to look like this json: 

    {
        "PersonId": <value>,
        "Link": {
            "BagId": <value>
         }
    }

    but if you want to bind to Linksmvccore, then change the form code to:

    <form asp-action="Create">
      <div class="form-group">
    <input name="BagId" value="@Model.Bag.Id" class="form-control" readonly="readonly" /> </div> <div class="form-group"> <select name="PersonID" asp-items="@Model.Options" onchage=""></select> </div> <div class="form-group"> <input type="submit" value="Create" class="btn btn-primary" /> </div> </form>

    note: you don't post a link number. 

    Thursday, May 7, 2020 9:36 PM
  • User1434241939 posted

     <input name="BagId" value="@Model.Bag.Id" class="form-control" readonly="readonly" />

    Yes, that works!  I just don't know why I can't use asp-for="@Model.Link.BagId" instead of name="BagId".  I am guessing it has to do with the way the tag helper binds (or doesn't bind) to the Action:

    public async Task<IActionResult> Create([Bind("LinkNumber,BagId,PersonId")] Linksmvccore linksmvccore)

    When I examine (F12) the HTML the View generates, I see name="Link.BagId" so I guess that won't match up with BagId in the bind statement.  I just don't like hard coding BagId in the view (not to mention the Bind).  As an aside, Bind doesn't even seem to support Intellisense so I can just type any old string in there.

    Friday, May 8, 2020 2:26 PM
  • User475983607 posted

    When I examine (F12) the HTML the View generates, I see name="Link.BagId" so I guess that won't match up with BagId in the bind statement.  I just don't like hard coding BagId in the view (not to mention the Bind).  As an aside, Bind doesn't even seem to support Intellisense so I can just type any old string in there.

    In my opinion, the hard part is understanding the model binding naming convention.  It seems you get that part which is good.

    If you decided that want to use tag helpers then you need to follow the naming rules.  If you tell the tag helper that the object values belongs to Link.BagId then the model binder looks for Link.BagId not BagId.  If you want the model binder to bind to BagId then move the BagId property to the root of the ViewModel.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Friday, May 8, 2020 5:16 PM