1

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>
  }
}
bigmac
  • 2,553
  • 6
  • 38
  • 61

2 Answers2

1

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.

Community
  • 1
  • 1
Shawn
  • 1,871
  • 2
  • 21
  • 36
  • Shawn, the status values are **not** fixed. Right now there are six possible values pulled form the DB, but the admin can add more if needs be. I'm not quite sure how to implement the custom display template using MVC3 and Razor views. Any chance you might be able to assist? Aren't the " – bigmac Dec 14 '11 at 17:06
  • The second update may be overkill, but it does show both the DisplayTemplate and nexted list. – Shawn Dec 14 '11 at 20:32
  • Shawn, thank you very much for you in-depth response and assistance. I wish I could accept two answers for my question. I am going to use your sample code to move my sample to using your DisplayTemplate so that I can reuse the data. You have been greatly helpful! – bigmac Dec 14 '11 at 21:08
  • Shawn, I have been trying all afternoon to get your code to work in my scenario because I like it better than what I have, but I have one issue... I can't seem to convert my database data into an List to use in your Test3 method in the controller. In your sample, you build the data directly as a type of List, but if my repository comes from the Entity Framework and is of type System.Data.Entity.DbSet, how can I convert it? – bigmac Dec 15 '11 at 00:37
  • The answer found here should do the trick: http://stackoverflow.com/questions/7312012/convert-model-to-viewmodel. You basically construct the ViewModel object in the LINQ query directly. If you end up needing to go the other direction, ViewModel->Model, I love Automapper (http://lostechies.com/jimmybogard/2009/06/30/how-we-do-mvc-view-models/) – Shawn Dec 15 '11 at 13:56
1

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>
  }
}
Romias
  • 13,783
  • 7
  • 56
  • 85
  • Romias, isn't string concatenation against best practices? I was hoping that I can define something site wide, that if I want do display a user, it's always in the format of "LastName, FirstName - - SomeOtherField" and then in the view, do a "foreach(var Status in Model.Status.DistinctValues){...display the member data for each status...}"? – bigmac Dec 14 '11 at 17:09
  • @bmccleary, to display a user, you can use a Display Template and the 2 foreach approach. That way the display template is reused. If you generate for each user some HTML you could concatenate it, but it is cleaner the display template option. – Romias Dec 14 '11 at 17:20
  • Thank you for the information, and I think I'm starting to understand it, but I'm still having a brain fart on being able to group by status. If my ViewModel is generating a generic list of members like "Doe, John, Active" and "Jane, Mary, Inactive", how can my view which uses an IEnumerable as the model, iterate through the distinct statuses? I'm sure this is a simple process, but I can't seem to wrap my head around it. For example, with that basic enumeration, how can I do "@foreach (var member in Model.DistinctStatus)" when there is no DistinctStatus method? – bigmac Dec 14 '11 at 18:03
  • You can do 'Model.Where(status == myStatus). The myStatus variable is one of the several status you have in your system. So in the outter for you loop through the available status codes, and in the inner one you pass the current status to the "Where" statement. CHECK the Update – Romias Dec 14 '11 at 18:19
  • Perfect! I got it figured out and it works perfectly. I will update my post with the working code so that others can use it if they want. One thing is that I had to add ToList() to my query so that the embedded foreach loops worked (otherwise they threw an Open Datareader exception). I appreciate your help! – bigmac Dec 14 '11 at 21:01