Search code examples
asp.net-mvcasp.net-coreasp.net-core-mvcrazor-pages

Create Form Razor Page with Nested Models


I have a razor page create form. The model it accepts is a NewHireClassModel:

public class NewHireClassModel
{
    public int new_hire_class_id { get; set; }
    public int district_id {  get; set; }
    public string district_name {  get; set; }
    public int class_size { get; set; }
    [DisplayFormat(DataFormatString = "MM dd yyyy")]
    public DateTime week_ending_date { get; set; }
    public bool is_cancelled {  get; set; }
    public string approval_status_name { get; set; }
    public int? new_requested_class_size { get; set; }
    public bool cancellationed_requested {  get; set; }
    public string created_by { get; set; }
    public DateTime created_on { get; set; }
    public string modified_by { get; set; }
    public DateTime modified_on { get; set; }
    public List<ClassLocationModel> class_locations { get; set; }
}

Notice the last item is a List of ClassLocations.

ClassLocationModel:

public class ClassLocationModel
{
    public int class_location_id { get; set; }
        
    [DisplayFormat(DataFormatString = "0:d")]
    public DateTime start_date { get; set; }
        
    [DisplayFormat(DataFormatString = "0: dd hh")]
    public DateTime start_time { get; set; }
        
    public int location_size { get; set; }//refers to the number of new hires at this location
    public string location_category { get; set; } //District Office, Hotel, or Other
    public List<LocationModel> location_choices { get; set; }
    public string location_address { get; set; }
    public string created_by { get; set; }
    public DateTime created_on { get; set; }
    public string modified_by { get; set; }
    public DateTime modified_on { get; set; }
    public bool is_deleted { get; set; }
}

I can create a list of ClassLocations and use that to create a NewHireClass and pass that to a view and show the items in the NewHireClass and the NewHireClass's ClassLocations. That's no problem.

What I can't figure out is how to build a create form that will accept a NewHireClass and the first location (new hire classes MUST have at least one ClassLocation). Here is my code so far.

Controller that builds a blank ClassLocation and NewHireClass with defaults and parameters specified from an HTTP Get:

public IActionResult CreateNewClassRequestForm(int _district_id, string _district_name, string _week_ending_date, int _class_size)
{
    List<LocationModel> _location_choices = new List<LocationModel>();
    List<ClassLocationModel> blank_location = new List<ClassLocationModel>();

    _location_choices.Add(new LocationModel { id = 1, location = "District Office" });
    _location_choices.Add(new LocationModel { id = 2, location = "Hotel" });
    _location_choices.Add(new LocationModel { id = 3, location = "Other" });

    blank_location.Add(new ClassLocationModel{
        location_size = _class_size
        , start_date = Convert.ToDateTime(_week_ending_date).AddDays(-6)
        , location_address = "tbd"
        , start_time = new DateTime(1,1,1,8,0,0)
        , location_choices = _location_choices
    });

    NewHireClassModel blank_request = new NewHireClassModel {
        new_hire_class_id = 0
        , district_id = _district_id
        , district_name = _district_name
        , class_size = _class_size
        , week_ending_date = Convert.ToDateTime(_week_ending_date)
        , approval_status_name = ""
        , cancellationed_requested = false
        , created_by = ""
        , created_on = DateTime.Now
        , modified_by = ""
        , modified_on = DateTime.Now
        , class_locations = blank_location
    };

    return View(blank_request);
}

The create form view:

@model OperationalForecast.Models.NewHireClassModel



<h4>ClassRequestModel</h4>
<hr />

<div class="row">
    <div class="col-md-4">
        <form asp-action="ProcessCreateNewClassRequest">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <input type=hidden asp-for="new_hire_class_id" class="form-control" />
            </div>
            <div class="form-group">
                <input type=hidden asp-for="district_id" class="form-control" />
            </div>
            <div class="form-group">
                <label asp-for="district_name" class="control-label"></label>
                <input readonly asp-for="district_name" class="form-control" />
                <span asp-validation-for="district_name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="week_ending_date" class="control-label"></label>
                <input readonly type="date" asp-for="week_ending_date" class="form-control" />
                <span asp-validation-for="week_ending_date" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="class_locations.ElementAt(0).start_date" class="control-label"></label>
                <input type="date" asp-for="class_locations.ElementAt(0).start_date" class="form-control" min="@Model.class_locations.ElementAt(0).start_date.ToString("yyyy-MM-dd")" max="@Model.week_ending_date.ToString("yyyy-MM-dd")" />
                <span asp-validation-for="class_locations.ElementAt(0).start_date" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="class_size" class="control-label"></label>
                <input asp-for="class_size" class="form-control" />
                <span asp-validation-for="class_size" class="text-danger"></span>
            </div>

            @foreach (var item in Model.class_locations.ElementAt(0).location_choices)
            {
                <br />
                <input asp-for="class_locations.ElementAt(0).location_category" type="radio" value="@item.location" /> @(item.location)
            }
            <br />


            <div class="form-group">
                <label asp-for="@Model.class_locations.ElementAt(0).location_address" class="control-label"></label>
                <input asp-for="@Model.class_locations.ElementAt(0).location_address" class="form-control" />
                <span asp-validation-for="@Model.class_locations.ElementAt(0).location_address" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="class_locations.ElementAt(0).start_time" class="control-label"></label>
                <input type="time" asp-for="class_locations.ElementAt(0).start_time" class="form-control" />
                <span asp-validation-for="class_locations.ElementAt(0).start_time" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type=hidden asp-for ="approval_status_name" class="form-control" />
            </div>
            <div class="form-group">
                <input type=hidden asp-for="cancellationed_requested" class="form-control" />
            </div>
            <div class="form-group">
                <input type=hidden asp-for="created_by" class="form-control" />
            </div>
            <div class="form-group">
                <input type=hidden asp-for="created_on" class="form-control" />
            </div>
            <div class="form-group">
                <input type=hidden asp-for="modified_by" class="form-control" />
            </div>
            <div class="form-group">
                <input type=hidden asp-for ="modified_on" class="form-control" />
            </div>
            <div class="form-group">
                <input type="submit" value="Initiate New Class Request" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

Highlighting two of the ClassLocation inputs:

            <div class="form-group">
                <label asp-for="@Model.class_locations.ElementAt(0).location_address" class="control-label"></label>
                <input asp-for="@Model.class_locations.ElementAt(0).location_address" class="form-control" />
                <span asp-validation-for="@Model.class_locations.ElementAt(0).location_address" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="class_locations.ElementAt(0).start_time" class="control-label"></label>
                <input type="time" asp-for="class_locations.ElementAt(0).start_time" class="form-control" />
                <span asp-validation-for="class_locations.ElementAt(0).start_time" class="text-danger"></span>

Notice I tried two different ways. Both worked great for populating the form with the default value. Neither saved the value back to a ClassLocation within the NewHireClass.


Solution

  • I was able to find a solution with some trial and error.

    Step 1: Add name attribute to <input> tag

                <div class="form-group">
                    <label asp-for="@Model.class_locations.ElementAt(0).location_address" class="control-label"></label>
                    <input asp-for="@Model.class_locations.ElementAt(0).location_address" **name="_location_address"** class="form-control" />
                    <span asp-validation-for="@Model.class_locations.ElementAt(0).location_address" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="class_locations.ElementAt(0).start_time" class="control-label"></label>
                    <input type="time" asp-for="class_locations.ElementAt(0).start_time" **name="_start_time"** class="form-control" />
                    <span asp-validation-for="class_locations.ElementAt(0).start_time" class="text-danger"></span>
                </div>
    

    That will pass the parameters to the method specified in the action attribute <form asp-action="ProcessCreateNewClassRequest">

    Step 2: take the arguments from the method and use to create a ClassLocation model and add that to the NewHireClass model

    public IActionResult ProcessCreateNewClassRequest(NewHireClassModel newhireclass, string _location_address, DateTime _start_date, DateTime _start_time, string _location_category)
    {
        List<ClassLocationModel> first_location = new List<ClassLocationModel>();
        first_location.Add(new ClassLocationModel { location_address = _location_address, start_date = _start_date, start_time = _start_time, location_category = _location_category });
    
        newhireclass.class_locations = first_location;
    
        List<NewHireClassModel> new_hire_classes = new List<NewHireClassModel>();
        new_hire_classes.Add(newhireclass);
    
    
        return View(new_hire_classes);
    }
    

    Note this function simply returns the NewHireClass model to a view to display to verify it is correctly populated for testing purposes.