Search code examples
c#asp.net-mvclinqasp.net-mvc-viewmodel

ViewModel with a SelectList and a LINQ Query result with a single reocrd


How can we populate a ViewModel with following two properties:

  1. A query result type from a LINQ query that returns only a single record
  2. A SelectList type

In other words, suppose we have a view that displays only a single movie based on the selected ID. And we want to assign a year of release to that movie from a dropdown list of years, how do I fill in the ???? in the following ViewModel and Controller?

Note: In the following ViewModel if I use myMovie property of type IQueryable<Movie> and assign this property of ViewModel to the query qrySingleMovie from following controller I get the error the name qrySingleMovie does not exist in the current context

Models

public class Movie
{
    public int ID { get; set; }
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; }
    public decimal Price { get; set; }
}

public class Year
{
  public int YearId {get; set;}
  public int MovieYear {get; set;}
}

ViewModel

public class MovieGenreViewModel
{
    public ???? myMovie;
    public SelectList MovieReleaseYears;
    public int ReleasedYr { get; set; }
}

Controller

public async Task<IActionResult> Index(int MovieID)
{
    // Use LINQ to get list of Release years.
    IQueryable<string> YearQuery = from m in _context.Years
                                    orderby m.MovieYear
                                    select m.MovieYear;

    //Following query selects only a single movie based on Primary Key ID
    var qrysingleMovie = from m in _context.Movies
                 where m.ID == movieID
                 select m;

    var movieReleaseYrVM = new MovieGenreViewModel();
    movieReleaseYrVM.MovieReleaseYears = new SelectList(await YearQuery.ToListAsync());
    movieReleaseYrVM.myMovie = ????

    return View(movieReleaseYrVM);
}

UPDATE

In my real project, the model has lots of properties and the corresponding View also displays most of those properties. I was thinking if there is a simpler way to define a ViewModel that does not contain all the properties of the model such as this ASP.NET Article defines a view model MovieGenreViewModel but there they are using List and in their View they are iterating through multiple Movies. I'm trying to mimic their example but in my case it's a single record (not the list of records) with a SelectList. How can I mimic that kind of scenario with a single record and not be able to include in View Model tons of model properties that model has?


Solution

  • Since you want to update a Movie record, all you need to get the movie record is it's unique Id. So why not add a MovieId property to your view model. If you prefer to show the MovieName in the UI, you may add a MovieName property as well.

    Remember view models are specific to views. It is not necessary to mimic your entire entity model when you create a view model. Add only those properties your view absolutely need.

    public class MovieGenreViewModel
    {
        public int MovieId;
        public string MovieName { set;get;}
        public SelectList MovieReleaseYears;
        public int ReleasedYr { get; set; }
    }
    

    Now in your GET action set these property values.

    public async Task<IActionResult> Index(int MovieID)
    {
        var vm = new MovieGenreViewModel();
    
        var movie =  _context.Movies.FirstOrDefault(x=>x.ID==movieID);
    
        if(movie==null)
           return Content("Movie not found"); // to do :Return a view with "not found" message
    
        IQueryable<string> YearQuery = from m in _context.Years
                                        orderby m.MovieYear
                                        select m.MovieYear;   
    
        // set the property values. 
        vm .MovieReleaseYears = new SelectList(await YearQuery.ToListAsync());
        vm .MovieId= movie.ID;
        vm .MovieName= movie.Name;
    
        return View(vm );
    }
    

    Now in your view, you keep the movie id in a hidden field so that when you submit the form, it will be submitted to the HttpPost action method which handles the form submit.

    @model MovieGenreViewModel
    @using(Html.BeginForm("AssignYear","Home"))
    {
        <p>@Model.MovieName</p>
        @Html.HiddenFor(s=>s.MovieId)
        @Html.DropDownListFor(d=>d.ReleasedYr, Model.MovieReleaseYears)
        <input type="submit"/>
    }
    

    You can use the same view model as your AssignYear action method parameter

    [HttpPost]
    public ActionResult AssignYear(MovieGenreViewModel model)
    {
      // check model.MovieId and model.ReleasedYr
      // to do : Save data and return something
    }
    

    If you do not prefer to add only those properties needed by the view to the view model, but want to show a tons of properties of the Movie, you may consider adding a new property of type Movie to your view model

    public class MovieGenreViewModel
    {
        public Movie Movie { set;get;}
        public SelectList MovieReleaseYears;
        public int ReleasedYr { get; set; }
    }
    

    Your existing LINQ statement returns a collection of one element. You cannot assign a collection of Movies to a property of type Movie. You may use the FirstOrDefault() method to get a single object.

    var movie =  _context.Movies.FirstOrDefault(x=>x.ID==movieID);
     //After null check
    vm.Movie = movie;