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

Calculated Property in MVC 5 ViewModel instead of Model Class


I have searched StackOverflow and MSDN and found several pages that I felt would help me, but once I tried implementing the solutions there was always something slightly different with what I needed so now I am here after hours of hair pulling.

Problem: I have a calculated property in my Model Class. The Class name is Render and represents a model for Photo Renderings. The calculated property will be used as a name for a Zip File I will package together on ONE VIEW for the user depending on which Render Detail page they are on. Obviously we don't want to do this in the Model because not every view or even a majority of views will be using it.

Question: How can I move this calculated property into my ViewModel?

Below you will see my current and working..

  • Render Class
  • View Model
  • Controller &
  • Applicable Razor Code

Render Class

public class Render
{
    public Render()
    {

    }

    public int RenderId { get; set; }
    public string ClientName { get; set; }
    public string Title { get; set; }
    public string JobId { get; set; }
    public string Comment { get; set; }

    public ICollection<Image> Images { get; set; }

    public string ZipFileName
    {
        get { return (ClientName + "-" + Title + "-" + JobId)
                .Replace(" ", string.Empty); }
    }
}

View Model

public class RenderDetailViewModel
{
    public IEnumerable<Render> AllRenders { get; set; }
    private readonly RenderLibContext _db = new RenderLibContext();

    public void PopulateRenderDetailModel(int id)
    {
        AllRenders = _db.Renders.Include("Images")
            .Where(r => r.RenderId == id)
            .ToList();
    }
}

Controller

public ActionResult Detail(int id=1)
{

    var render = _db.Renders.Include("Images")
                    .Include(r => r.Comments.Select(c => c.CommentImages))
                    .Include("VenueType").Include("ScreenCount")
                    .FirstOrDefault(r => r.RenderId == id);

    var model = new RenderDetailViewModel()
    {
        RenderId = render.RenderId,
        ClientName = render.ClientName,
        Title = render.Title,
        JobId = render.JobId,
        RenderMonth = render.RenderMonth,
        RenderYear = render.RenderYear,
        Comment = render.Comment,
        Venue = render.Venue,
        Mapping = render.Mapping,
        DefaultImageId = render.DefaultImageId

    };

    return View(model);
}

Razor Code (Abbreviated)

@model RenderLib.ViewModels.RenderDetailViewModel
...
@foreach (var group in Model.Images.Select(i => i.Version).Distinct().ToList())
{
  <a href="?Set=@group">Set @counter</a>
  <a href="/Renders/Download/@Model.RenderId?fn=@Model.ZipFileName">Download</a>
  ...
}
@foreach (var images in Model.Images
.Where(i => i.RenderId == Model.RenderId && i.Version == Request.QueryString["Set"]))
{
   <li data-thumb="/ImageStore/@images.Path">
     <a href="javascript:void(0)">
       <img src="~/ImageStore/@images.Path" />
     </a>
   </li>
}

Everything above this line works fine, but my calculated property is in the wrong place, I would like it to be in my ViewModel

I know that a lot of you would like to see someone who is asking a question like this to show anything they have already tried to make sure I'm not just asking for answers without attempting something first so below is an example of one of the many attempts to create a calculated property in my ViewModel:

View Model (My Attempt)

public class RenderDetailViewModel
{
    public IEnumerable<Render> RenderById { get; set; }
    private static readonly RenderLibContext _db = new RenderLibContext();

    private readonly Render _model;
    public RenderDetailViewModel(Render r)
    { _model = r; }

    public string ClientName { get { return _model.ClientName; } }
    public string Title { get { return _model.Title; } }
    public string JobId { get { return _model.JobId; } }

    public string ZipFileName
    {
        get { 
            return (ClientName + "-" + Title + "-" + JobId)
                        .Replace(" ", string.Empty); 
        }
    }

    public void PopulateRenderDetailModel(int id)
    {
        RenderById = _db.Renders.Include("Images")
            .Where(r => r.RenderId == id)
            .ToList();
    }
}

This attempt screwed up my controller and expected me to pass in an argument. That is not what I was aiming to do. Any help would be greatly appreciated. I hope I included everything needed to aid anyone who might be able to help.


Solution

  • A view model should not contain any database logic. It should have simple properties only. Also there is no way that you could use this in any edit scenario where you need to post back because a) you do not have a parameterless constructor (will throw an exception) and you properties have no setters. Just copy the properties from your data model to your view model and do the mapping in the controller (and then you can delete the ZipFileName in the data model)

    public class RenderViewModel
    {
      public int RenderId { get; set; }
      public string ClientName { get; set; }
      ....
      public IEnumerable<Image> Images { get; set; }
      public string ZipFileName
      {
        get { return (ClientName + "-" + Title + "-" + JobId)
                .Replace(" ", string.Empty); }
      }
    }
    

    Controller

    public ActionResult Detail(int id)
    {
      var render = db.Renders.Include("Images").Where(r => r.RenderId == id).FirstOrDefault();
      var model = new RenderViewModel()
      {
        RenderId = render.RenderId,
        ClientName = render.ClientName,
        ....,
        Images = render.Images.Select(...), // not sure what your trying to do here
      });
      return View(model);
    }
    

    View

    @model RenderViewModel
    @Html.DisplayFor(m => m.ClientName)
    ....
    @foreach (var group in renders.Images)
    {
      // better to use helpers to generate the link
      @Html.ActionLink("Download", "Download", "Renders", new { ID = Model.RenderId, fn = Model.ZipFileName })
    }
    

    Note, I'm confused by what you trying to do when selecting the images associated with each render. For each image you generating an identical link (based on the RenderId and ZipFileName of the render). Is there a property in Image that should included in the route parameters so each link downloads something different?