Jason Ebersey
Jason Ebersey

Reputation: 647

Error passing ViewModel to View using EF Core MVC

I have a ViewModel passing to Views and am getting a strange error:

InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'System.Collections.Generic.List1[BizDevHub.Models.Intermediary]', but this ViewDataDictionary instance requires a model item of type 'System.Collections.Generic.IEnumerable1[BizDevHub.ViewModels.IntermediaryViewModel]'.

I cannot understand why as it seems that I am passing the ViewModel to the View and not the Model.

My Models

public class Intermediary
{
    public int IntermediaryID
    public string RegisteredName { get; set; }
    public string TradingName { get; set; }
    public DateTime CreationDate { get; set; }
    public string CreatedBy { get; set; }

    public ICollection<Branch> Branches { get; set; }
}

public class Branch
{
    public int BranchID { get; set; }
    public string Name { get; set; }

    [DisplayName("Creation Date")]
    public DateTime CreationDate { get; set; }

    [StringLength(100)]
    [DisplayName("Created By")]
    public string CreatedBy { get; set; }
}

My ViewModel

public class IntermediaryViewModel
{
    public int IntermediaryID { get; set; }
    [Required,StringLength(150),DisplayName("Registered Name")]
    public string RegisteredName { get; set; }

    [Required, StringLength(150), DisplayName("Registered Name")]
    public string TradingName { get; set; }
    public int Registration { get; set; }
    public int VATNumber { get; set; }

    [Required]
    public int FSPNumber { get; set; }

    [DisplayName("Creation Date")]
    public DateTime CreationDate { get; set; }

    [StringLength(100)]
    [DisplayName("Created By")]
    public string CreatedBy { get; set; }

    public int BranchID { get; set; }
    public ICollection<Branch> Branches { get; set; }
}

My Views

@model IEnumerable<BizDevHub.ViewModels.IntermediaryViewModel>

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

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.RegisteredName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.TradingName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Registration)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.VATNumber)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.FSPNumber)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.CreationDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.CreatedBy)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.BranchID)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.RegisteredName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.TradingName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Registration)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.VATNumber)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FSPNumber)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.CreationDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.CreatedBy)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.BranchID)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.IntermediaryID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.IntermediaryID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.IntermediaryID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>


@model BizDevHub.ViewModels.IntermediaryViewModel

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

<h2>Create</h2>

<h4>Intermediary</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="RegisteredName" class="control-label"></label>
                <input asp-for="RegisteredName" class="form-control" />
                <span asp-validation-for="RegisteredName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="TradingName" class="control-label"></label>
                <input asp-for="TradingName" class="form-control" />
                <span asp-validation-for="TradingName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Registration" class="control-label"></label>
                <input asp-for="Registration" class="form-control" />
                <span asp-validation-for="Registration" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="VATNumber" class="control-label"></label>
                <input asp-for="VATNumber" class="form-control" />
                <span asp-validation-for="VATNumber" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FSPNumber" class="control-label"></label>
                <input asp-for="FSPNumber" class="form-control" />
                <span asp-validation-for="FSPNumber" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="CreatedBy" class="control-label"></label>
                <input asp-for="CreatedBy" class="form-control" />
                <span asp-validation-for="CreatedBy" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="BranchID" class="control-label"></label>
                <input asp-for="BranchID" class="form-control" />
                <span asp-validation-for="BranchID" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label class="control-label col-md-2" for="DepartmentID">Branch</label>
                <div class="col-md-10">
                    @Html.DropDownList("DepartmentID", null, new {@class="form-group"})
                    @Html.ValidationMessageFor(model => model.BranchID)
                </div>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

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

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

My Controller

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using BizDevHub.Data;
using BizDevHub.Models;
using BizDevHub.ViewModels;

namespace BizDevHub.Controllers
{
    public class IntermediariesController : Controller
    {
        private readonly BizDevHubContext _context;

        public IntermediariesController(BizDevHubContext context)
        {
            _context = context;
        }

        // GET: Intermediaries
        public async Task<IActionResult> Index()
        {
            return View(await _context.Intermediaries.ToListAsync());
        }

        // GET: Intermediaries/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var intermediary = await _context.Intermediaries
                .FirstOrDefaultAsync(m => m.IntermediaryID == id);
            if (intermediary == null)
            {
                return NotFound();
            }

            return View(intermediary);
        }

        // GET: Intermediaries/Create
        public IActionResult Create()
        {
            PopulateBranchDropDownList();
            return View();
        }

        // POST: Intermediaries/Create
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost, ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("IntermediaryID,RegisteredName,TradingName,Registration,VATNumber,FSPNumber,CreationDate,CreatedBy,BranchID")] IntermediaryViewModel intermediary)
        {
            if (ModelState.IsValid)
            {
                _context.Add(intermediary);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            PopulateBranchDropDownList(intermediary.BranchID);
            return View(intermediary);
        }

        // GET: Intermediaries/Edit/5
        public async Task<IActionResult> Edit(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var intermediary = await _context.Intermediaries.FindAsync(id);
            if (intermediary == null)
            {
                return NotFound();
            }
            return View(intermediary);
        }

        // POST: Intermediaries/Edit/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
        // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
        [HttpPost, ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(int id, [Bind("IntermediaryID,RegisteredName,TradingName,Registration,VATNumber,FSPNumber,CreationDate,CreatedBy,BranchID")] IntermediaryViewModel intermediary)
        {
            if (id != intermediary.IntermediaryID)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                try
                {
                    _context.Update(intermediary);
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!IntermediaryExists(intermediary.IntermediaryID))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
                return RedirectToAction(nameof(Index));
            }
            PopulateBranchDropDownList(intermediary.BranchID);
            return View(intermediary);
        }

        // GET: Intermediaries/Delete/5
        public async Task<IActionResult> Delete(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var intermediary = await _context.Intermediaries
                .FirstOrDefaultAsync(m => m.IntermediaryID == id);
            if (intermediary == null)
            {
                return NotFound();
            }

            return View(intermediary);
        }

        // POST: Intermediaries/Delete/5
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var intermediary = await _context.Intermediaries.FindAsync(id);
            _context.Intermediaries.Remove(intermediary);
            await _context.SaveChangesAsync();
            return RedirectToAction(nameof(Index));
        }

        private bool IntermediaryExists(int id)
        {
            return _context.Intermediaries.Any(e => e.IntermediaryID == id);
        }

        private void PopulateBranchDropDownList(object selectedBranch = null)
        {
            var branchesQuery = from d in _context.Branch
                                orderby d.Name
                                select d;
            ViewBag.DepartmentID = new SelectList(branchesQuery, "DepartmentID", "Name", selectedBranch);
        }
    }
}

Any help will be appreciated!

Upvotes: 0

Views: 1427

Answers (2)

sam
sam

Reputation: 1985

I see couple of issues with your code.

  1. Your views are taking ViewModel/s as input. But you are returning Model object in Index action. So, in order to fix the error that is shown in the title of this POST, please update your index action to convert colleciton of model objects to collection of viewmodel objects.
public async Task<IActionResult> Index()
{
    var intermediaryViewModels = ModelsToViewModelsConversion(_context.Intermediaries.ToListAsync());
    return View(intermediaryViewModels);
}

private async List<IntermediaryViewModel>() ModelsToViewModelsConversion(IEnumerable<Intermediary> models)
{
    // conversion code goes here.
    // return collection of IntermediaryViewModel;
}
  1. Create GET action (// GET: Intermediaries/Create) is not returning any object, but view is expecting instance of IntermediaryViewModel. So, you need to modify your Create GET Action as below:
// GET: Intermediaries/Create
public IActionResult Create()
{
  // PopulateBranchDropDownList();
  var branchesQuery = from d in _context.Branch
      orderby d.Name
      select d;
  return View(new IntermediaryViewModel{Branches = branchesQuery.AsEnumerable() });
}

And update your Create.cshtml as below to display the branches:

<div class="form-group">
    <label class="control-label col-md-2" for="DepartmentID">Branch</label>
    <div class="col-md-10">
        @Html.DropDownList("DepartmentID", new SelectList(Model.Branches, "DepartmentID", "Name"), new { @class = "form-group" })
        @Html.ValidationMessageFor(model => model.BranchID)
    </div>
</div>

What I noticed is you have mixed up models and viewmodels (for example IntermediaryViewModel has Branch model instead of BranchViewModel) and in Create POST action, you are adding viewmodel to context models (entities).

All of your code is good if you have taken care of model-viewmodel conversion process via some operator or something of that sort. But if that is the case, you don't get the error that you are seeing now which making me to think there is no such conversion going on in your code.

Updated your code to include Conversion from Model to ViewModel.

public class Intermediary
{
    public int IntermediaryID  { get; set; }
    public string RegisteredName { get; set; }
    public string TradingName { get; set; }
    public DateTime CreationDate { get; set; }
    public string CreatedBy { get; set; }

    public ICollection<Branch> Branches { get; set; }
}

public class Branch
{
    public int BranchID { get; set; }
    public string Name { get; set; }

    // Generally Model classes should not have any idea on Display related stuff.
    // [DisplayName("Creation Date")]
    public DateTime CreationDate { get; set; }

    [StringLength(100)]
    // [DisplayName("Created By")]
    public string CreatedBy { get; set; }
}

public class IntermediaryViewModel
{
    // I just created common expression so it can be used from Index as well as Details actions.
    // Feel free to remove this common expression and have the code inside controller actions if you prefer that way. 
    /// <summary>
    /// Lambda expression converting Intermediary to IntermediaryViewModel
    /// </summary>
    public static readonly Expression<Func<Intermediary, IntermediaryViewModel>> AsIntermediaryViewModel =
    i => new IntermediaryViewModel{
        // Please add other required properties mapping here. I just showed couple 
        IntermediaryID = i.IntermediaryID,
        RegisteredName = i.RegisteredName,
        BranchID       = i.BranchID,
        // if you want you can populate Branches (by Intermediary) here like this in a single database call as we have mentioned Include in actions
        Branches = i.Branches.AsQueryable().Select(b => new BranchViewModel{ BranchID = b.BranchID}).ToList()
    };

    public int? IntermediaryID { get; set; }

    [Required,StringLength(150),DisplayName("Registered Name")]
    public string RegisteredName { get; set; }

    [Required, StringLength(150), DisplayName("Registered Name")]
    public string TradingName { get; set; }
    public int Registration { get; set; }
    public int VATNumber { get; set; }

    [Required]
    public int FSPNumber { get; set; }

    // As now you have separate ViewModel for display purposes, 
    // you can have string version of CreationDate which is (DateTime to string) converted as per your requirement.
    [DisplayName("Creation Date")]
    public DateTime CreationDate { get; set; }

    [StringLength(100)]
    [DisplayName("Created By")]
    public string CreatedBy { get; set; }
    // Since you are defining BranchId as non-nullable, you will need to default to some existing BranchId so, your (create) view will show this branch when page is loaded. Otherwise make it nullable int (int ?) so, dropdown would display your message "Select Branch" when BranchId is null
    public int BranchID { get; set; }
    public ICollection<BranchViewModel> Branches { get; set; }
}

// since you are using Branches to populate drop down, other audit properties are not required here.
// Does not hurt having them if you want.
public class BranchViewModel
{
    public int BranchID { get; set; }
    public string Name { get; set; }    
}




// GET: Intermediaries
public async Task<IActionResult> Index()
{
    return View(await _context.Intermediaries.Include(i => i.Branches).Select(IntermediaryViewModel.AsIntermediaryViewModel).ToListAsync());
}

// GET: Intermediaries/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }
    // I just renamed the variable to speak what it represents. 
    var intermediaryViewModel = await _context.Intermediaries.Include(i => i.Branches).Select(IntermediaryViewModel.AsIntermediaryViewModel)
        .FirstOrDefaultAsync(m => m.IntermediaryID == id);
    if (intermediaryViewModel == null)
    {
        return NotFound();
    }

    return View(intermediaryViewModel);
}

private void PopulateBranchDropDownList(object selectedBranch = null)
{
    ViewBag.BranchList = from d in _context.Branch
                        orderby d.Name
                        select new BranchViewModel
                        {
                            BranchID = d.BranchID,
                            Name = d.Name
                        }.ToList();

    ViewBag.BranchID = selectedBranch;  //Set to some predefined BranchID selection, so when page is loaded, dropdown will be defaulted to this value.
}

Please use the same for other controller actions. No need to change anything in the views as they are already taking ViewModels except that Branch dropdown.

Please notice I have replaced DepartmentID with BranchID

@{
    var Branchist = new SelectList(ViewBag.LocList, "Id", "Text");
    int? BranchID = ViewBag.BranchID ?? (int ?)null; // please test this line.
}

<div class="form-group">
    <label class="control-label col-md-2" for="BranchID">Branch</label>
    <div class="col-md-10">
        @Html.DropDownList(@BranchID, @Branchist , "Select Branch", new {@class="form-group"})
        @Html.ValidationMessageFor(model => model.BranchID)
    </div>
</div>

Upvotes: 1

Bryan Lewis
Bryan Lewis

Reputation: 5977

For:

return View(await _context.Intermediaries.ToListAsync());

and

return View(intermediary);

You seem to be passing the View objects of type Intermediary, not IntermediaryViewModel, unless your context is somehow converting them in other code. I don't see any code that's mapping the entity (Intermediary) to the ViewModel (IntermediaryViewModel). Perhaps use AutoMapper or something similar to convert your EF entities into the ViewModel.

Upvotes: 2

Related Questions