Search code examples
asp.net-coredrop-down-menurazor-pagesviewbagdisplayfor

.Net Core Razor page with multiple drop-down's for the same entity


Is there any way I can have multiple drop-down menu displaying the data from the same database entity on the same razor page without creating several classes with new IDs (e.g. ResourceID; ResourceID1; ResourceID2)

I am able to display the drop down with the appropriate data from the MS SQL database in the 'Create.cshtml' and 'Edit.cshtml' razor pages, but the chosen data when saved displays the ID of the chosen resource instead of the name of the resource in the 'Index.cshtml', 'Detail.cshtml' and 'Delete.cshtml' views.

The Resource model:

namespace ProjectReporting.Models
{
    public class Resource
    {
        public int ID { get; set; }

        [Display(Name = "First Name")]
        public string FirstName { get; set; }

        [Display(Name = "Last Name")]
        public string LastName { get; set; }

        [Display(Name = "Long Name")]
        public string LongName { get; set; }

        [DefaultValue(true)]
        [Display(Name = "Active")]
        public bool IsActive { get; set; } = true;

        [Display(Name = "Is Manager")]
        public bool IsManager { get; set; }

        [Display(Name = "Is Forecast Owner")]
        public bool IsForecastOwner { get; set; }

        public ICollection<Project> Projects { get; set; }

    }
}

The Project model:

namespace ProjectReporting.Models
{
    public class Project
    {
        public int ID { get; set; }

        [Display(Name = "ID")]
        public int PID { get; set; }

        [Display(Name = "Project Name")]
        public string ProjectName { get; set; }

        [Display(Name = "Forecast Owner")]
        public int ResourceID { get; set; }
        public Resource Resource { get; set; }

        [Display(Name = "DSM")]
        public int? ResourceID1 { get; set; }

        public ICollection<ProjectComment> ProjectComments { get; set; }

    }
}

The Create.cshtml page:

@page
@model ProjectReporting.Pages.Projects.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Project</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Project.PID" class="control-label"></label>
                <input asp-for="Project.PID" class="form-control" />
                <span asp-validation-for="Project.PID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Project.ProjectName" class="control-label"></label>
                <input asp-for="Project.ProjectName" class="form-control" />
                <span asp-validation-for="Project.ProjectName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Project.ResourceID" class="control-label"></label>
                <select asp-for="Project.ResourceID" class="form-control" asp-items="ViewBag.ResourceID"><option value="" default="" selected="">-- Select --</option></select>
            </div>
            <div class="form-group">
                <label asp-for="Project.ResourceID1" class="control-label"></label>
                <select asp-for="Project.ResourceID1" class="form-control" asp-items="ViewBag.ResourceID1"><option value="" default="" selected="">-- Select --</option></select>
            </div>
            <div class="form-group form-check">
                <label class="form-check-label">
                    <input class="form-check-input" asp-for="Project.IsArchived" /> @Html.DisplayNameFor(model => model.Project.IsArchived)
                </label>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

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

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

The Create.cshtml page:

namespace ProjectReporting.Pages.Projects
{
    public class CreateModel : PageModel
    {
        private readonly ProjectReporting.Data.ApplicationDbContext _context;

        public CreateModel(ProjectReporting.Data.ApplicationDbContext context)
        {
            _context = context;
        }

        public IActionResult OnGet()
        {
            ViewData["OrganisationID"] = new             SelectList(_context.ProjectType.Where(a => a.IsActive == true), "ID", "TypeName");
            ViewData["ResourceID"] = new SelectList(_context.Resource.Where(a => a.IsActive & a.IsForecastOwner == true), "ID", "LongName");
            ViewData["ResourceID1"] = new SelectList(_context.Resource.Where(a => a.IsActive == true), "ID", "LongName");
            return Page();
        }

        [BindProperty]
        public Project Project { get; set; }

        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Project.Add(Project);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}

The Index.cshtml page:

@page
@model ProjectReporting.Pages.Projects.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-page="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Project[0].PID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Project[0].Organisation)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Project[0].ProjectName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Project[0].Resource)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Project[0].ResourceID1)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Project[0].IsArchived)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Project) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.PID)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Organisation.OrgName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ProjectName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Resource.LongName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => c)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.IsArchived)
            </td>
            <td>
                <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

The item.Resource.LongName works fine for the first resource, but I would like the same to happen with the item.Resource.LongName.

The Index.cshtml.cs

namespace ProjectReporting.Pages.Projects
{
    public class IndexModel : PageModel
    {
        private readonly ProjectReporting.Data.ApplicationDbContext _context;

        public IndexModel(ProjectReporting.Data.ApplicationDbContext context)
        {
            _context = context;
        }

        public IList<Project> Project { get;set; }

        public async Task OnGetAsync()
        {
            Project = await _context.Project
                .Include(p => p.Resource).ToListAsync();
        }
    }
}

I have set the FK in the migration file to create the DB to be able to retrieve the data and would like to avoid having to create one class file by resource.

table.ForeignKey(
     name: "FK_Project_Resource_ResourceID",
     column: x => x.ResourceID,
     principalTable: "Resource",
     principalColumn: "ID",
     onDelete: ReferentialAction.Restrict);
table.ForeignKey(
     name: "FK_Project_Resource_ResourceID1",
     column: x => x.ResourceID1,
     principalTable: "Resource",
     principalColumn: "ID",
     onDelete: ReferentialAction.Restrict);

The result shows the right data in the drop-down and the correct Resource ID selected when saved. However, the index, details and delete page only display the ResourceID instead of the LongName. If I use Resource.LongName for the second ResourceID1, it rightly displays the same LongName than for ResourceID.

How can I have multiple resource drop-down on the page that point to the same entity and display the LongName on the Index, Detail and Delete pages?


Solution

  • It is not clear about your relationships between Resource and Project, and you could not use both ResourceID and ResourceID1 for a Resource property for restoring different resources.

    For many-to-many relationships, you could refer to

    https://learn.microsoft.com/en-us/ef/core/modeling/relationships#many-to-many

    A workaround is that you just get all resources in handler and retrieve it in view:

    Index.cshtml.cs:

    public class IndexModel : PageModel
    {
        private readonly ProjectReporting.Data.ApplicationDbContext _context;
    
        public IndexModel(ProjectReporting.Data.ApplicationDbContext context)
        {
            _context = context;
        }
    
        public IList<Project> Project { get;set; }
        public IList<Resource> Resources { get; set; }
    
        public async Task OnGetAsync()
        {
            Resources = await _context.Resource.ToListAsync();
            Project = await _context.Projects
                .Include(p => p.Resource).ToListAsync();
        }
    }
    

    Index.cshtml:

    <td>
        @Html.DisplayFor(modelItem => item.Resource.LongName)
    </td>
    <td>
        @{ 
            var resource = Model.Resources.FirstOrDefault(r => r.ID == item.ResourceID1);
        }
        @resource.LongName
    </td>