0

So I am working on a project that contains a centralized communications page that contains all the “notes” from throughout the site. Right now I am able to bring the notes in on a per-model basis, but I am stuck with three groups that require not just the note, but also needs to be divided by parent so that each group of notes are allocated to the proper parent.

As a quick breakdown:

The top level is that of companies. Several sections within the company can have their own notes (multiple notes, so separate notes table for each section), and so there are several notes tables brought in from this level.

The next level is a “cycle” that the company can go through. Only the most recent cycle will ever get dealt with, so this ends up being rather simple. This level contains the most sections with separate notes tables, but by having the current cycle contained in a claim, I can filter for just the current cycle.

The lowest level involves a few tables that hang off of the cycle. Since these are not as limited as the cycle, I need to be able to bring in not just these tables, but also the notes for every entry in those tables. While the tables can be filtered by the Cycle claim, the notes tables don’t have that information.

My problem is with this lowest level. I do not know how to create a “multi-IEnumerable” such that both parent and child data can be attached to a single viewModel container. This is preventing me from creating a Linq Lambda expression that can fill that container. Once I have the container filled, the rest ought to be easy -- I just create a pair of nested for loops that extract first the parent (just the name of the table row) and thet the child (all the notes entries from the notes table for that table row).

In other words, I need a left join to fill a single viewmodel, and I cannot do this until I find out how to create an IEnumerable that holds both models.

Parent
+----+------+
| id | name |
+----+------+
|  1 | John |
|  2 | Mike |
-------------

Child
+----+-----+-----------------+------------+
| id | Pid | note            | date       |
+----+-----+-----------------+------------+
|  1 |   2 | Mike's notes    | 2016-01-15 |
|  2 |   1 | John’s notes    | 2016-02-22 |
|  3 |   2 | More Mike’s     | 2016-02-24 |
|  4 |   2 | Yet more Mike’s | 2016-03-01 |
-------------------------------------------

Page output

            John
2016-02-22 John’s Notes
[text field] [submit]

            Mike
2016-01-15 Mike’s notes
2016-02-24 More Mike’s
2016-03-01 Yet more Mike’s
[text field] [submit]

Plus, please keep in mind that this is just one of many models being brought into the page, right now I have almost 20 on the same page, so my controller start out with var viewModel = new CommunicationsViewModel(); and has multiple models being brought in by attaching them to the viewModel by way of IEnumerables in the Controller's model. My only problem is that I need to attach two models to one IEnumerable, so that my JOIN query can succeed in dumping content into a viewModel container.

Right now, my most promising Lambda expression is thus:

viewModel.Active = await db.Activity
  .Join(
    db.ActivityNotes.Include(y => y.NotesStatus),
    act => act.ActivityId,
    actnot => actnot.ActivityId,
    (act, actnot) => new { Activity = act, ActivityNotes = actnot }
  )
  .Where(act => act.CycleId == cycleId)
  .ToListAsync();

But of course I cannot do anything with it until I figure out how to combine both Activity and ActivityNotes into a single IEnumerable called Active.

Edit: For Erik Philips:

For my communications page, I have the following model laid out:

public class CommunicationsViewModel {
  ...more IEnumerated models...
  public IEnumerable<Activity> Active { get; set; }
  public IEnumerable<ActivityNotes> ActiveComm { get; set; }
}

Notice the last two entries, parent and child. The parent needs to be filtered for a cycle Guid, but that's it.

My OnModelCreating:

#region Activity
modelBuilder.Entity<Activity>().HasMany(x => x.ActivityNotes).WithRequired(x => x.Activity).HasForeignKey(x => x.ActivityId).WillCascadeOnDelete(true);
modelBuilder.Entity<Activity>().HasKey(x => x.ActivityId);
modelBuilder.Entity<Activity>().Property(x => x.ActivityId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Activity>().Property(x => x.CycleId).IsRequired();
modelBuilder.Entity<Activity>().Property(x => x.ActivityName).HasColumnType("nvarchar").HasMaxLength(50).IsOptional();
modelBuilder.Entity<Activity>().Property(x => x.ActivityDate).HasColumnType("date").IsRequired();
... redundant content removed ...
#endregion
#region ActivityNotes
modelBuilder.Entity<ActivityNotes>().HasKey(x => x.ActivityNotesId);
modelBuilder.Entity<ActivityNotes>().Property(x => x.ActivityNotesId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<ChampionNotes>().Property(x => x.Notes).IsRequired();
#endregion

My Index, highly abridged to contain only the relevant piece:

public async Task<ActionResult> Index() {
  var cycleId = new Guid(User.GetClaimValue("CWD-Cycle"));
  var viewModel = new CommunicationsViewModel();
  viewModel.Active = await db.Activity.Where(x => x.CycleId == cycleId).Where(x => x.Active == true).ToListAsync();
  ...more viewModelled content brought in...
  return View(viewModel);
}

And there is the content of my view:

@foreach(var item in Model.Active) {
  @Html.DisplayFor(modelItem => item.ActivityName)
  foreach(var subItem in Model.ActiveComm) {
    @Html.DisplayFor(modelItem => subItem.Notes)
  }
}

…Aaaand it basically fails at the second foreach, the classic “Object reference not set to an instance of an object”. Which is catchall for ASP.NET saying “whoops, you dun goofed; good luck finding out where, tho.”

René Kåbis
  • 842
  • 2
  • 9
  • 28
  • 1
    You don't have to join if you tell EF there is a [foreign key and setup navigation properies](https://msdn.microsoft.com/en-us/data/jj713564.aspx)... – Erik Philips Mar 08 '16 at 01:28
  • I was extremely careful to include foreign key references in both EF 6 as well as Fluent Validation. This means that my code-first DB has full relationships, all with the proper cascade/ignore settings as required. So what you are saying is that all I have to do is bring in my Activities and then do the double `for` loop to spit out the notes grouped by activity? Somehow that doesn’t sound right; that there should be more work involved in the setup, so that the notes can be explicitly targeted in their `for` loop. – René Kåbis Mar 08 '16 at 01:29

2 Answers2

2

I do not know how to create a “multi-IEnumerable” such that both parent and child data can be attached to a single viewModel container.

I don't understand this statement... at all. Here is a ViewModel with both parent and child models:

public class MyModel
{
  public IEnumerable<Parent> Parents { get; set; }
  public IEnumerable<Child> Children { get; set; }
}

Although really something like the following seems like what you want and is still simple.

public class MyModel
{
  public IEnumerable<Parent> Parents { get; set; }
}

public class Parent
{
  public int Id { get; set; }
  public string Name { get; set; }
  public ICollection<Child> Children { get; set; }
}

public class Child
{
  public int Id { get; set; }
  public int Pid { get; set; }
  public Parent Parent { get; set; }
  public string Note { get; set; }
  public DateTime Date { get; set; }
}

public class MyDbContext : DbContext
{
  public override void OnModelCreating(DbModelBuilder mb)
  {
    mb.Entity<Parent>()
      .HasKey(p => p.Id);

    mb.Entity<Child>()
      .HasKey(c => c.Id)
      .HasRequired(c => c.Parent)
      .WithMany(p => p.Children)
      .HasForeignKey(c => c.Pid);
  }
}

public ActionResult SomeMethod()
{
  var viewModel = new MyModel
  {
    Parents = db.Set<Parent>()
      .Where(p => p.Id == 1)
      .Include(p => p.Children)
      .AsNoTracking()
      .ToList();
  };

  return View(viewModel);
}

Object reference not set to an instance of an object”. Which is catchall for ASP.NET saying “whoops, you dun goofed; good luck finding out where, tho.”

No it only means one thing and it ALWAYS means the same thing. It means you are asking the RunTime Engine to access a variable that has never been assigned a value (most of the time it's the programmers error).

You have this model:

public class CommunicationsViewModel {
  public IEnumerable<Activity> Active { get; set; }
  public IEnumerable<ActivityNotes> ActiveComm { get; set; }
}

and the only code you've given is:

viewModel.Active = await db.Activity
  .Where(x => x.CycleId == cycleId)
  .Where(x => x.Active == true)
  .ToListAsync();

So it's extremely apparent that you never set:

Model.ActiveComm

This is my last edit to this post. You're missing basic c# understanding and basic debugging skills. I recommend you take some time and review http://www.asp.net/web-pages/overview/testing-and-debugging/introduction-to-debugging and other resources available at http://www.asp.net. Good luck!

Community
  • 1
  • 1
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • Okay, I don’t want to actually set any parent, I want to bring back an array with all parents (of the current cycle) and all associated children from each parent. As per my original, I need to filter parents that are only a part of the current cycle, and I need to bring back the children of all filtered parents. – René Kåbis Mar 08 '16 at 05:39
  • Plus, I need to have the children directly grouped with their associated parent, not elsewhere on the page. – René Kåbis Mar 08 '16 at 05:43
  • *I need to filter parents that are only a part of the current cycle* so update the `where()` statement, it can't be that hard... *I need to bring back the children of all filtered parents* it already does that with `Include()`. – Erik Philips Mar 08 '16 at 06:03
  • I have updated my original post. It fails with the view, the second `foreach` that is supposed to bring in the child associated with the parent in the first `foreach`. – René Kåbis Mar 08 '16 at 06:09
0

The key, I just found out, is entirely within how you link the two foreach loops together, as well as how you bring in that particular chunk of data.

To begin with, look only at the parent, but include the child:

viewModel.Parent = await db.Parent
  .Where(x => x.ForeignKey == limitingFactor)
  .Include(x => x.Child)
  .ToListAsync();

Point: only the parent has to be present in the ViewModel of the page as an IEnumerable, which is what I got horribly hung up on -- I thought both had to be brought in. They don’t. Since we are only explicitly bringing in the parent into the viewModel, as seen above, any child content rides in on the parent’s coattails (to spindle a metaphor) via the .Include(). They do not need to be referenced at all as an IEnumerable in the ViewModel of the page.

This is pretty well what Erik Philips said in his post above, full attribution to Erik for pointing that out in the ActionResult. However, at this point I still didn’t know how to “grab” the child data on a per-parent basis… for all I could tell it wasn’t being brought in. However, when mucking around with my view, I stumbled across the correct method.

By pure chance, I made the second foreach loop build off of the first loop, and it worked.

The first, outside one would be for the parent:

@foreach(var parent in Model.Parent){
  @Html.DisplayFor(modelItem => parent.RandomFieldNameInParent)

And since each parent needs to have a list of its children:

foreach(var child in parent.Child){

Notice how the child table is being brought in? We have already filtered the parent, and by this point we are looking at just one parent entry, and in this internal foreach loop, we are looking for all children in the child table related to that parent. That is why it is parent.Child, and not Model.Child. it is the Child content (any number of children) related to the parent from the outside foreach loop.

The resulting code looks like this:

@foreach(var parent in Model.Parent){
  @Html.DisplayFor(modelItem => parent.RandomFieldNameInParent)
  foreach(var child in parent.Child){
    @Html.DisplayFor(modelItem => child.RandomFieldNameInChild)
  }
}

This is exactly the end result I was looking for from the beginning. Pardon me while I go pour myself a stiff one; the simplicity of the answer is out of all proportion to the effort that went into finding it.

René Kåbis
  • 842
  • 2
  • 9
  • 28