bigmac
bigmac

Reputation: 2553

MVC Passing Grouped Data to View

I am an MVC noob, but trying to implement best practices and keeping as little code in a view as possible. The issue I'm running into is that I have a list of members and their associated statuses. I can pass this information to the view and render each member and their status in one line, but I want the view to group the data and look like the following:

Status: Active
 - John Doe
 - Mary Jane
 - etc...
Status: Inactive
 - Mark Smith
 - etc...

I don't think it's best practice to have some sort of multi-level for loop in a view (correct me if I'm wrong), and that I should have some sort of partial view for the member information (right now, just FirstName and LastName, but will eventually be more complex) and then some sort of main view for the grouping by status that then renders the partial view for each member. I am also trying to use the ViewModel approach to keep clean views. Any suggestions for how to do this according to best practices are appreciated! Also, any comments on my current code (organization, cleanliness, etc.) are welcome.

--- If you want to see my current code, it is as follows ---

Here's the Controller that sends the results of the query to the view:

namespace MyApp.Web.Controllers
{
  public class MemberController : Controller
  {
    private IMemberQueries _memberQuery;

    public MemberController(IMemberQueries memberMemberQuery)
    {
      _memberQuery = memberMemberQuery;
    }

    public ViewResult Index()
    {
      return View(_memberQuery.GetMembersWithStatus());
    }
  }
}

Here's the Query code:

namespace MyApp.Web.ViewModels
{
  public class MemberQueries : IMemberQueries
  {
    private IMemberRepository _memberRepository;

    public MemberQueries(IMemberRepository memberMemberRepository)
    {
      _memberRepository = memberMemberRepository;
    }

    public IEnumerable<MembersIndexViewModel> GetMembersWithStatus()
    {
      return
        _memberRepository.Member.Include(m => m.Status).Select(
          m => new MembersIndexViewModel { FirstName = m.FirstName, LastName = m.LastName, Status = m.Status.StatusName });
    }
  }
}

Here's my ViewModel to limit the data going to the view:

namespace MyApp.Web.ViewModels
{
  public class MembersIndexViewModel
  {
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public string Status { get; set; }
  }
}

Here's the view and how it uses the viewmodel to display each member's name and status, but it's not grouped:

@model IEnumerable<MyApp.Web.ViewModels.MembersIndexViewModel>
<h2>Member List</h2>
@foreach (var member in Model)
{
  <div>
    <h3>@member.LastName, @member.FirstName - @member.Status</h3>
  </div>
}

UPDATE: Here's what I had to change for it to work based on Romias's assistance

Update the MemberQueries to call ToList() to cause immediate calling of the query:

public IEnumerable<MembersIndexViewModel> GetMembersWithStatus()
{
  return
    _memberRepository.Member.Include(m => m.Status).Select(
      m => new MembersIndexViewModel { FirstName = m.FirstName, LastName = m.LastName, Status = m.Status.StatusName }).ToList();
}

Here's the updated view that now works:

<h2>Member List</h2>
@foreach (string status in Model.Select(x => x.Status).Distinct())
{
  <h2>@status:</h2>
  string memberStatus = status;
  foreach (MembersViewModel member in Model.Where(m => m.Status == memberStatus))
  {
    <div>@member.LastName, @member.FirstName</div>
  }
}

Upvotes: 1

Views: 4525

Answers (2)

Shawn
Shawn

Reputation: 1891

If the status values are fixed, one option is to modify your ViewModel to contain two lists of Members -- one for active and another for Inactive.

Regardless of whether or not you keep your combined list or split it, you could use a custom display template to render the actual member. That would keep the view clean and allow you to add future display updates in a single location.

UPDATE:

Here is a link that shows how to create display templates. What is the @Html.DisplayFor syntax for?

You basically add a sub-folder to the appropriate View folder (Home, Shared, etc) named "DisplayTemplates". Create in that folder ModelName.cshtml files that have a @model as the first line. From then out out, its the standard Razor/Html you would put inside your for-loop.

You could get more complicated by having your ViewModel contain Dictionary<string, List<MembersIndexViewModel>>. The string key would be your status and the value would be the list of members with that status.

I would start with the DisplayTemplate before tackling the nested List.

---- UPDATE 2: A working example -----

The base object for a member:

public class MemberViewModel
{
    public string Status { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    /* ... other properties ... */
}

A ViewModel for the page itself that could contain other properties as necessary:

public class MemberIndexViewModel
{
    // view may need additional info outside of the status list
    public string SomeOtherData { get; set; } 

    public Dictionary<string, List<MemberViewModel>> MembersByStatus { get; set; }

    public MemberIndexViewModel()
    {
        MembersByStatus = new Dictionary<string, List<MemberViewModel>>();            
    }
}

A mocked up function to return your list of members:

private List<MemberViewModel> MockDataFromDB()
{
    List<MemberViewModel> members = new List<MemberViewModel>();

    for (var i = 0; i < 20; i++)
    {
        var m = new MemberViewModel();

        if (i < 10)
            m.Status = "Active";
        else if (i < 15)
            m.Status = "Inactive";
        else
            m.Status = "Unknown";

        m.FirstName = "First" + i.ToString();
        m.LastName = "Last" + i.ToString();

        members.Add(m);
    }

    return members;
}

The controller action that gets the data from the db and then builds the appropriate viewmodel by sorting members into the appropriate list:

[HttpGet]
public ActionResult Test3()
{
    var members = MockDataFromDB(); // get the data from the DB

    var vm = new MemberIndexViewModel();

    vm.SomeOtherData = "Something else the view may need.";


    foreach (var m in members)
    {
        if (!vm.MembersByStatus.ContainsKey(m.Status))
            vm.MembersByStatus.Add(m.Status, new List<MemberViewModel>());

        vm.MembersByStatus[m.Status].Add(m);
    }

    return View(vm);
}

The display template for the member object located in Views-->Home-->DisplayTemplates-->MemberViewModel.cshtml:

@model ViewModel.MemberViewModel

<div>@Model.FirstName @Model.LastName</div>

And finally, the view that ties it all together:

@model ViewModel.MemberIndexViewModel

<span>SomeOtherData: </span><span class="display-field">@Html.DisplayFor(model => model.SomeOtherData)</span>

<hr />

@foreach (var group in Model.MembersByStatus)
{ 
    <fieldset>
        <legend>@group.Key</legend>
        @Html.DisplayFor(m => group.Value)
    </fieldset>
}

This may be overkill for your scenario, but at least you can see how it could work.

Upvotes: 1

Romias
Romias

Reputation: 14133

Once you have the "status" in your models, you could do 2 foreach... One looping over the ACTIVE ones and other looping for the inactive.

or

you can loop one time, concatenating the html of active users in a string variable and the others in another variable. Then just display the concatenation of each variable.

EDIT: If the status are not fixed, you could make a nested foreach. The first one looping by status options, and in the inner loop filtering the user collection by the status.

This way you can add a label with the status, and then all the users. Then another label with the next status and their users.

The users can be displayed using a Display Template.

SECOND EDIT: pseudo code

@model IEnumerable<MyApp.Web.ViewModels.MembersIndexViewModel>
<h2>Member List</h2>

@foreach (var myStatus in ()ViewBag.StatusList){
  <h3>@myStatus</h3>

  @foreach (var member in Model.Where(status == myStatus ))
  {
    <div>
      <h3>@member.LastName, @member.FirstName - @member.Status</h3>
    </div>
  }
}

Upvotes: 1

Related Questions