Passing ViewModel with Dictionary properties to View throws NullReferenceException
I have a ViewModel that includes two Dictionary properties, Genres and Producers, which I want to send to the View. However, when I try to loop through the Dictionary properties in the View, I get a NullReferenceException error. The data appears to be passing normally until that point. How can I fix this issue?
public class SeriesViewModel : GenericViewModel
{
[Required(ErrorMessage = "A video link must be specified")]
public string VideoLink { get; set; }
[Required(ErrorMessage = "A producer must be selected")]
public int ProducerId { get; set; }
[Required(ErrorMessage = "At least 1 genre must be selected")]
public int PrimaryGenreId { get; set; }
public int? SecondaryGenreId { get; set; }
// Navigation properties
public string ProducerName { get; set; }
public string PrimaryGenreName { get; set; }
public string SecondaryGenreName { get; set; }
// When I try to initialize the Genres property with "new Dictionary<int, string>()" the ViewModel brings the Genres list empty
public Dictionary<int, string> Producers { get; set; }
public Dictionary<int, string> Genres { get; set; }
}
@model Application.ViewModels.SeriesViewModels.SeriesViewModel
<h1>@Model.Producers[24]</h1>
<div class="mb-3">
<label asp-for="ProducerId" class="form-label">Producer</label>
<select asp-for="ProducerId" class="form-select">
<option>Select a producer</option>
@foreach (var producer in Model.Producers)
{
<option value="@producer.Key">@producer.Value</option>
}
</select>
<span asp-validation-for="ProducerId" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="PrimaryGenreId" class="form-label">Primary Genre</label>
<select asp-for="PrimaryGenreId" class="form-select">
<option>Select the first genre</option>
@foreach (var genre in Model.Genres)
{
<option value="@genre.Key">@genre.Value</option>
}
</select>
<span asp-validation-for="PrimaryGenreId" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="SecondaryGenreId" class="form-label">Secondary Genre</label>
<select asp-for="SecondaryGenreId" class="form-select">
<option>Select the secondary genre</option>
@foreach (var genre in Model.Genres)
{
<option value="@genre.Key">@genre.Value</option>
}
</select>
<span asp-validation-for="SecondaryGenreId" class="text-danger"></span>
</div>
Entity Framework Core
ASP.NET
-
JasonPan - MSFT 5,796 Reputation points • Microsoft Vendor
2024-09-24T04:37:48.1233333+00:00 Hi @Emmanuel Campos,
Please change your
SeriesViewModel
like below. If still facing the issue, please share your code in the controller .public class SeriesViewModel : GenericViewModel { public SeriesViewModel() { Producers = new Dictionary<int, string>(); Genres = new Dictionary<int, string>(); } [Required(ErrorMessage = "A video link must be specified")] public string VideoLink { get; set; } [Required(ErrorMessage = "A producer must be selected")] public int ProducerId { get; set; } [Required(ErrorMessage = "At least 1 genre must be selected")] public int PrimaryGenreId { get; set; } public int? SecondaryGenreId { get; set; } public string ProducerName { get; set; } public string PrimaryGenreName { get; set; } public string SecondaryGenreName { get; set; } public Dictionary<int, string> Producers { get; set; } public Dictionary<int, string> Genres { get; set; } }
Best Regards
Jason
-
Emmanuel Campos 0 Reputation points
2024-09-24T06:28:04.1233333+00:00 I have tried that already and these were my results, same as what the comment says, the List with no valuews
// When I try to initialize the Genres property with "new Dictionary<int, string>()" the ViewModel brings the Genres list empty public Dictionary<int, string> Producers { get; set; } public Dictionary<int, string> Genres { get; set; }
-
JasonPan - MSFT 5,796 Reputation points • Microsoft Vendor
2024-09-24T06:33:46.5266667+00:00 Hi @Emmanuel Campos,
Please share all the related files with me, I need to reproduce the issue in my local, then I can help you as soon as possible.
Something like your SeriesViewModel.cs file, .cshtml file, and xxxController.cs file and so on, which can reproduce the issue in my side.
Thanks for your patience.
Best Regards
Jason
-
Emmanuel Campos 0 Reputation points
2024-09-24T16:39:36.1933333+00:00 Since my project has quite a large directory tree which I'm not used to and this is my first time facing an issue that has taken me so long I'm not sure exactly what to send you or if it's better sending you the whole project. Also I don't exactly know how to send them to you
-
Emmanuel Campos 0 Reputation points
2024-09-24T18:33:05.07+00:00 I'll just write the code that I consider relevant
Application/Repository/Common/GenericRepository.cs
using Application.Repository.Interfaces; using Database.Contexts; using Microsoft.EntityFrameworkCore; namespace Application.Repository.Common { public abstract class GenericRepository<T> : IRepository<T> where T : class { protected readonly ApplicationContext _dbContext; private readonly string _entityName; public GenericRepository(ApplicationContext dbContext, string entityName) { _dbContext = dbContext; _entityName = entityName; } public async Task AddAsync(T entity) { await _dbContext.Set<T>().AddAsync(entity); await _dbContext.SaveChangesAsync(); } public async Task UpdateAsync(T entity) { _dbContext.Entry(entity).State = EntityState.Modified; await _dbContext.SaveChangesAsync(); } public async Task DeleteAsync(T entity) { _dbContext.Set<T>().Remove(entity); await _dbContext.SaveChangesAsync(); } public virtual async Task<List<T>> GetAllAsync() { return await _dbContext.Set<T>().ToListAsync(); } public virtual async Task<T> GetByIdAsync(int id) { var entity = await _dbContext.Set<T>().FindAsync(id); if (entity == null) { throw new KeyNotFoundException($"{this._entityName} with ID {id} not found."); } return entity; } } }
Application/Repository/SeriesRepository.cs
using Application.Repository.Common; using Database.Contexts; using Database.Entities; using Microsoft.EntityFrameworkCore; namespace Application.Repository { public class SeriesRepository : GenericRepository<Series> { public SeriesRepository(ApplicationContext dbContext) : base(dbContext, "Series") { } public override async Task<Series> GetByIdAsync(int id) { var entity = await _dbContext.Series .Include(s => s.Producer) .Include(s => s.PrimaryGenre) .Include(s => s.SecondaryGenre) .FirstOrDefaultAsync(s => s.Id == id); if (entity == null) { throw new KeyNotFoundException($"Series with ID {id} not found."); } return entity; } public override async Task<List<Series>> GetAllAsync() { return await _dbContext.Series .Include(s => s.Producer) .Include(s => s.PrimaryGenre) .Include(s => s.SecondaryGenre) .ToListAsync(); } public List<Producer> GetAllProducers() { return _dbContext.Producers.ToList(); } public List<Genre> GetAllGenres() { return _dbContext.Genres.ToList(); } } }
Application/Services/GenericService.cs
using Application.Repository.Interfaces; namespace Application.Services.Common { public abstract class GenericService<TEntity, TViewModel, TRepository> where TEntity : class where TRepository : IRepository<TEntity> { protected readonly TRepository _repository; protected GenericService(TRepository repository) { _repository = repository; } public virtual async Task<List<TViewModel>> GetAllViewModels() { var entityList = await _repository.GetAllAsync(); return entityList.Select(e => MapToViewModel(e)).ToList(); } public virtual async Task<TViewModel> GetByIdViewModel(int id) { var entity = await _repository.GetByIdAsync(id); return MapToViewModel(entity); } public virtual async Task Add(TViewModel vm) { var entity = MapToEntity(vm); await _repository.AddAsync(entity); } public virtual async Task Update(TViewModel vm) { var entity = MapToEntity(vm); await _repository.UpdateAsync(entity); } public virtual async Task Delete(TViewModel vm) { var entity = MapToEntity(vm); await _repository.DeleteAsync(entity); } protected abstract TEntity MapToEntity(TViewModel vm); protected abstract TViewModel MapToViewModel(TEntity entity); } }
Application/Services/SeriesService.cs
using Application.Repository; using Application.Services.Common; using Application.Services.Interfaces; using Application.ViewModels.SeriesViewModels; using Database.Contexts; using Database.Entities; namespace Application.Services { public class SeriesService : GenericService<Series, SeriesViewModel, SeriesRepository>, IService<SeriesViewModel> { public SeriesService(ApplicationContext dbContext) : base(new SeriesRepository(dbContext)) { } protected override Series MapToEntity(SeriesViewModel vm) { return new Series { Id = vm.Id, Name = vm.Name, Description = vm.Description, ImageLink = vm.ImageLink, VideoLink = vm.VideoLink, ProducerId = vm.ProducerId, PrimaryGenreId = vm.PrimaryGenreId, SecondaryGenreId = vm.SecondaryGenreId }; } protected override SeriesViewModel MapToViewModel(Series entity) { var producers = this.GetAllProducers(); var genres = this.GetAllGenres(); return new SeriesViewModel { Id = entity.Id, Name = entity.Name, Description = entity.Description, ImageLink = entity.ImageLink, VideoLink = entity.VideoLink, ProducerId = entity.ProducerId, PrimaryGenreId = entity.PrimaryGenreId, SecondaryGenreId = entity.SecondaryGenreId, ProducerName = entity.Producer?.Name, PrimaryGenreName = entity.PrimaryGenre?.Name, SecondaryGenreName = entity.SecondaryGenre?.Name, Producers = producers.ToDictionary(p => p.Id, p => p.Name), Genres = genres.ToDictionary(g => g.Id, g => g.Name) }; } public List<Producer> GetAllProducers() { return _repository.GetAllProducers(); } public List<Genre> GetAllGenres() { return _repository.GetAllGenres(); } } }
Application/ViewModels/Common/GenericViewModel.cs
using System.ComponentModel.DataAnnotations; namespace Application.ViewModels.Common { public class GenericViewModel { public int Id { get; set; } [Required(ErrorMessage = "A name must be specified")] public string Name { get; set; } [Required(ErrorMessage = "An image link must be specified")] public string ImageLink { get; set; } [Required(ErrorMessage = "A description must be specified")] public string Description { get; set; } } }
Application/ViewModels/SeriesViewModel.cs
using Application.ViewModels.Common; using System.ComponentModel.DataAnnotations; namespace Application.ViewModels.SeriesViewModels { public class SeriesViewModel : GenericViewModel { public SeriesViewModel() { Producers = new Dictionary<int, string>(); Genres = new Dictionary<int, string>(); } [Required(ErrorMessage = "A video link must be specified")] public string VideoLink { get; set; } [Required(ErrorMessage = "A producer must be selected")] public int ProducerId { get; set; } [Required(ErrorMessage = "At least 1 genre must be selected")] public int PrimaryGenreId { get; set; } public int? SecondaryGenreId { get; set; } // Navigation properties public string ProducerName { get; set; } public string PrimaryGenreName { get; set; } public string SecondaryGenreName { get; set; } public Dictionary<int, string> Genres { get; set; } public Dictionary<int, string> Producers { get; set; } } }
MainProject/Common/GenericCRUDController.cs
using Application.Helpers; using Application.Services.Interfaces; using CineBox.Controllers.Interfaces; using Microsoft.AspNetCore.Mvc; namespace CineBox.Controllers.Common { public abstract class GenericCRUDController<TService, TViewModel> : Controller, ICRUDController<TViewModel> where TService : IService<TViewModel> { protected readonly TService _service; protected readonly string _redirectController; protected readonly string _redirectAction; protected GenericCRUDController(TService service, string redirectController, string redirectAction) { _service = service; _redirectController = redirectController; _redirectAction = redirectAction; } [HttpGet] public virtual IActionResult Create() => View("Save", Activator.CreateInstance<TViewModel>()); [HttpPost] public virtual async Task<IActionResult> Create(TViewModel vm) { if (!ModelState.IsValid) { return View("Save", vm); } await _service.Add(vm); return RedirectToRoute(new { controller = _redirectController, action = _redirectAction }); } [HttpGet] public virtual async Task<IActionResult> Update(int id) { if (!ValidationHelper.ValidateViewModelId(id)) { return BadRequest("Invalid ViewModel ID."); } return View("Save", await _service.GetByIdViewModel(id)); } [HttpPost] public virtual async Task<IActionResult> Update(TViewModel vm) { if (!ModelState.IsValid) { return View("Save", vm); } await _service.Update(vm); return RedirectToRoute(new { controller = _redirectController, action = _redirectAction }); } [HttpGet] public virtual async Task<IActionResult> Delete(int id) => View("Delete", await _service.GetByIdViewModel(id)); [HttpPost] public virtual async Task<IActionResult> Delete(TViewModel vm) { await _service.Delete(vm); return RedirectToRoute(new { controller = _redirectController, action = _redirectAction }); } } }
MainProject/Controllers/SeriesController.cs
using Application.Services; using Application.ViewModels.SeriesViewModels; using CineBox.Controllers.Common; using Database.Contexts; namespace CineBox.Controllers { public class SeriesController : GenericCRUDController<SeriesService, SeriesViewModel> { public SeriesController(ApplicationContext dbContext) : base(new SeriesService(dbContext), "Site", "Series") { } } }
MainProject/Views/Series/Save.cshtml (these are the foreach loops that throw the NullReferenceException)
@model Application.ViewModels.SeriesViewModels.SeriesViewModel ... code <h1>@Model.Producers[11]</h1> @* These H1 tags show up normally *@ <h1>@Model.Genres[22]</h1> <div class="mb-3"> <label asp-for="ProducerId" class="form-label">Producer</label> <select asp-for="ProducerId" class="form-select"> <option>Select a producer</option> @foreach (var producer in Model.Producers) { <option value="@producer.Key">@producer.Value</option> } </select> <span asp-validation-for="ProducerId" class="text-danger"></span> </div> <div class="mb-3"> <label asp-for="PrimaryGenreId" class="form-label">Primary Genre</label> <select asp-for="PrimaryGenreId" class="form-select"> <option>Select the first genre</option> @foreach (var genre in Model.Genres) { <option value="@genre.Key">@genre.Value</option> } </select> <span asp-validation-for="PrimaryGenreId" class="text-danger"></span> </div> <div class="mb-3"> <label asp-for="SecondaryGenreId" class="form-label">Secondary Genre</label> <select asp-for="SecondaryGenreId" class="form-select"> <option>Select the secondary genre</option> @foreach (var genre in Model.Genres) { <option value="@genre.Key">@genre.Value</option> } </select> <span asp-validation-for="SecondaryGenreId" class="text-danger"></span> </div>
I apologize my response is so long, I tried to give as much context of my code as posible. I'm open to any suggestion regarding the way I ask for help in forums.
-
Emmanuel Campos 0 Reputation points
2024-09-24T19:59:04.55+00:00 So the solution was rather stupid (I'm not sure if it's the best one but this is a project for a college asignment so I can improve that later).
I implemented a method to populate both dictionaries right before the ViewModel is passed onto the View and removed:
namespace Application.Helpers { public static class SeriesHelper { public static void PopulateProducersAndGenres(SeriesViewModel vm, SeriesService seriesService) { vm.Producers = seriesService.GetAllProducers().ToDictionary(p => p.Id, p => p.Name); vm.Genres = seriesService.GetAllGenres().ToDictionary(g => g.Id, g => g.Name); } } }
I removed the assignment of the Dictionary values in the SeriesService
protected override SeriesViewModel MapToViewModel(Series entity) { return new SeriesViewModel { Id = entity.Id, Name = entity.Name, Description = entity.Description, ImageLink = entity.ImageLink, VideoLink = entity.VideoLink, ProducerId = entity.ProducerId, PrimaryGenreId = entity.PrimaryGenreId, SecondaryGenreId = entity.SecondaryGenreId, ProducerName = entity.Producer?.Name, PrimaryGenreName = entity.PrimaryGenre?.Name, SecondaryGenreName = entity.SecondaryGenre?.Name, }; }
And implemented it in my SeriesController like this
[HttpGet] public override IActionResult Create() { var vm = new SeriesViewModel(); SeriesHelper.PopulateProducersAndGenres(vm, _seriesService); return View("Save", vm); } [HttpPost] public override async Task<IActionResult> Create(SeriesViewModel vm) { if (!ModelState.IsValid) { SeriesHelper.PopulateProducersAndGenres(vm, _seriesService); return View("Save", vm); } await _service.Add(vm); return RedirectToRoute(new { controller = "Site", action = "Series" }); }
-
JasonPan - MSFT 5,796 Reputation points • Microsoft Vendor
2024-09-25T04:21:59.0933333+00:00 Hi @Emmanuel Campos,
Please change your
MapToViewModel
method like below and try again.protected override SeriesViewModel MapToViewModel(Series entity) { var vm = new SeriesViewModel { Id = entity.Id, Name = entity.Name, Description = entity.Description, ImageLink = entity.ImageLink, VideoLink = entity.VideoLink, ProducerId = entity.ProducerId, PrimaryGenreId = entity.PrimaryGenreId, SecondaryGenreId = entity.SecondaryGenreId, ProducerName = entity.Producer?.Name, PrimaryGenreName = entity.PrimaryGenre?.Name, SecondaryGenreName = entity.SecondaryGenre?.Name }; SeriesHelper.PopulateProducersAndGenres(vm, _service as SeriesService); return vm; }
And please also provide more details about where you are facing the NullReferenceException, we need more debug details.
Best Regards
Jason
Sign in to comment