Properties stuck one level down - c#

I am not having a problem so much with the query as accessing the data or setting it up so I can pass it to the view.
Here's the expression
var distinctReplies = pd.Project.ProjectDoc
.SelectMany(i => i.Comment
.SelectMany(k => k.CommentReply
.Select(u => u.User)
).Distinct()
).Select(g => new {FirstName = g.FirstName, LastName = g.LastName, UserID = g.UserID})
.ToList();
After this expression I want to concat it with another one that is getting values from the same user model, I want to assign distinctReplies to a ViewBag variable and then be able to loop though it and do this
foreach (var user in #ViewBag.distinctReplies) in a razor view.
However, to actually get at the values I have to do distinctReplies.Select(i => i.FirstName). Not sure how to deal with this.

I would suggest to create some ViewModel object for populating the query result. To be able to access the properties inside the #foreach loop.
Model
public class UserVM
{
public int UserID{get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
}
Updated query
var distinctReplies = ....
.Select(g => new UserVM {FirstName = g.FirstName,
LastName = g.LastName,
UserID = g.UserID}).ToList();
Then, in the view, you will need to add cast to IEnumerable<UserVM>.
View
#foreach (var user in (IEnumerable<UserVM>) ViewBag.distinctReplies)

Use this:
#foreach (var reply in (IEnumerable)ViewBag.distinctReplies)
{
}
You cannot enumerate an instance of a dynamic. Cast it to an IEnumerable for the statement to be allowed.
See this question for a more thorough explanation.

Related

How do I group on one of two possible fields using LINQ?

I am trying to get the latest contact with a given user, grouped by user:
public class ChatMessage
{
public string SentTo { get; set; }
public string SentFrom { get; set; }
public string MessageBody { get; set; }
public string SendDate { get; set; }
}
The user's contact info could either be in SentTo or SentFrom.
List<ChatMessage> ecml = new List<ChatMessage>();
var q = ecml.OrderByDescending(m => m.SendDate).First();
would give me the latest message, but I need the last message per user.
The closest solution I could find was LINQ Max Date with group by, but I cant seem to figure out the correct syntax. I would rather not create multiple List objects if I don't have to.
If the user's info is in SentTo, my info will be in SentFrom, and vice-versa, so I do have some way of checking where the user's data is.
Did I mention I was very new to LINQ? Any help would be greatly appreciated.
Since you need to interpret each record twice - i.e. as a SentTo and a SentFrom, the query becomes a bit tricky:
var res = ecml
.SelectMany(m => new[] {
new { User = m.SentFrom, m.SendDate }
, new { User = m.SentTo, m.SendDate }
})
.GroupBy(p => p.User)
.Select(g => new {
User = g.Key
, Last = g.OrderByDescending(m => m.SendDate).First()
});
The key trick is in SelectMany, which makes each ChatMessage item into two anonymous items - one that pairs up the SentFrom user with SendDate, and one that pairs up the SentTo user with the same date.
Once you have both records in an enumerable, the rest is straightforward: you group by the user, and then apply the query from your post to each group.
It should be pretty easy, look at this code:
string username = "John";
var q = ecml.Where(i=>i.SentFrom == username || i.SentTo == username).OrderByDescending(m => m.SendDate).First();
It simply filter your collection be choosing items which either SentFrom or SentTo is equal to username.

Code a SQL projection and mapping at the same time?

This works to carve out a DDL object from an Address object from our database:
public class DDL {
public int? id { get; set; }
public string name { get; set; }
}
List<DDL> mylist = Addresses
.Select( q => new DDL { id = q.id, name = q.name })
.ToList();
However, we'd like to keep our POCO to ViewModel mappings in a single place outside of our MVC controller code. We'd like to do something like this:
List<DDL> mylist = Addresses
.Select( q => new DDL(q)) // <-- constructor maps POCO to VM
.ToList();
But SQL cannot use the constructor function. The object initializer above doesn't use functions to map fields. Of course you could do .AsEnumerable().Select( q => new DDL(q)), but this selects all the fields in SQL (including the data), sends it to C#, then C# carves out the fields we need (terribly inefficient to transfer data we don't need.)
Any suggestions? We happen to be using Entity Framework 6 to pull data.
All you need is to define the expression somewhere and use it. For example, in your ViewModel as a static read-only field.
public class SomeViewModel
{
public static readonly Expression<Func<SomeEntity, SomeViewModel>> Map = (o) => new SomeViewModel
{
id = o.id,
name = o.name
}
public int id { get; set; }
public string name { get; set; }
}
// Somewhere in your controller
var mappedAddresses = Addresses.Select(SomeViewModel.Map);
I personally made myself a little static Mapper that keeps all the maps and use them for me. The maps are declared in a static initializer in all my ViewModels. The result gives me something that feels like AutoMapper, yet doesn't require the lib or the complicated mapping code (but also won't do any magic for you).
I can write something like this:
MyCustomMapper.Map<Entity, ViewModel>(entity);
and it's overloaded to accept IEnumerables, IQueryables and a single ViewModel. I also added overloads with only 1 generic type (the entity) that accept a type parameter. This was a requirement for me.
You can use anonymous types to restrict what to select from the DB and then use those fields to construct your object :
List<DDL> mylist = Addresses
.Select( q => new { id, name })
.AsEnumerable()
.Select(i => new DDL(i.id, i.name) // <- On the Enumerable and not on the Queryable
.ToList();
Are you against using 3rd party libraries? Automapper's QueryableExtensions does exactly what you want.
List<DDL> mylist = Addresses
.Project().To<DDL>()
.ToList();
It even has nice features like being able to filter on the transformed object and that filter being performed server side.
List<DDL> mylist = Addresses
.Project().To<DDL>()
.Where(d => d.name = "Smith") //This gets translated to SQL even though it was performed on DDL.
.ToList();

Passing table join results to a view?

I have the following ASP.Net MVC controller action which joins 2 tables:-
public ActionResult PersonNotes()
{
var model = db.Notes
.Join(db.People, p => p.NotesListId, n => n.NotesListId,
((note, person) => new { note, person })).ToList();
return View(model);
}
And in my view I have the following model declaration:-
#model IEnumerable<Tuple<Models.Note, Models.Person>>
I get the following error:-
System.InvalidOperationException: The model item passed into the dictionary is of type 'System.Collections.Generic.List`1[<>f__AnonymousTypef`2[Models.Note,Models.Person]]', but this dictionary requires a model item of type System.Collections.Generic.IEnumerable`1[System.Tuple`2[Models.Note,Models.Person]]'.
I realise that I can just use a ViewModel and a Select() within my join, but it would far more convenient to just have access to all items without having to create a ViewModel.
What is the correct declaration in my view, or is what I am trying to achieve not possible this way?
You are returning an anonymous object and your view wants a model with a Tuple. The anonymous type is generated by the compiler and is not available at the source code level.
Try changing your statement to create an IEnumerable<Tuple<Models.Note, Models.Person>> with Tuple.Create:
var model = db.Notes
.Join(db.People, p => p.NotesListId, n => n.NotesListId,
((note, person) => Tuple.Create(note, person))).ToList();
See Tuple.Create.
If you are using Linq to Entities or Entity Framework then you will need to either iterate the IQueryable into a Tuple or use a class.
var model = db.Notes
.Join(db.People, p => p.NotesListId, n => n.NotesListId,
((note, person) => new { note, person }))
.AsEnumerable()
.Select(x => Tuple.Create(x.note, x.person))
.ToList();
Or
Create a class to hold the Person and Note.
public class PersonNote
{
public Person Person { get; set; }
public Note Note { get; set; }
}
Change the statement to use the new PersonNote.
var model = db.Notes
.Join(db.People, p => p.NotesListId, n => n.NotesListId,
((note, person) => new PersonNote { Note = note, Person = person }))
.ToList();
Change the model.
#model IEnumerable<PersonNote>

c# MVC Pass Group Data to View

I have a view where I display a model, but at the model I want to also display a small table of data from another model that comes from the following linq in the controller
var StudentRatings = db.LearnerRatings
.Where(i => i.LessonId == id)
.GroupBy(i => i.Rating)
.Select(group => new { Rating = group.Key, TotalCount = group.Count() });
ViewBag.Ratings = StudentRatings;
I'm already returning another model from the controller, so have added this to the ViewBag.
I thought I would be able to iterate through these with a foreach
foreach (var ratings in ViewBag.Ratings){
#ratings.TotalCount
}
but get an error - Additional information: 'object' does not contain a definition for 'TotalCount'
I take it this has to be cast? If so what does it get casted to?
Is this the best approach for the above?
Anonymous types should not traverse method boundaries: that is, your View doesn't know what type ratings is.
Change your anonymous type to a named struct or class, then you can do it like so:
class Rating {
public Int32 Rating { get; set; }
public Int32 TotalCount { get; set; }
}
...
.Select(group => new Rating { Rating = group.Key, TotalCount = group.Count() });
...
foreach(Rating rating in ViewBad.Ratings) {
#rating.TotalCount
}

Filter List in Html.EditorFor()

I was just wondering if there was a way to apply a filter to the #Html.EditorFor() when using it for a collection.
Lets say I have the following code:
EditorTemplates/Foo
#model Foo
#Html.EditorFor(f = f.FooDetails)
EditorTemplates/FooDetail
#model FooDetail
#Html.LabelFor(f => f.Group)
#Html.LabelFor(f => f.Name)
#Html.Editorfor(f => f.Name)
My first thought would be to do:
#Html.EditorFor(f = f.FooDetails.Where(x => x.Group == group)
However, after crossing my fingers while trying it out, I was reminded that life is not always simple and it gave back an InvalidOperationException due to "Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions."
Any suggestions on how I would properly approach the problem at hand?
Html.EditorFor has an overload that allows you to specify additional view data. You can pass the criteria through this parameter.
So your EditorFor becomes this:
#Html.EditorFor(f = f.FooDetails, new { Group = group})
And your editor template* becomes this:
#model FooDetail
#if ((string)ViewData["Group"] == Model.Group) {
#Html.LabelFor(f => f.Group)
#Html.LabelFor(f => f.Name)
#Html.Editorfor(f => f.Name)
}
*this assumes group is a string, change type as required
One way to approach it would be to declare a separate variable outside of the Html.EditorFor.
Instead of using #Html.EditorFor(f = f.FooDetails.Where(x => x.Group == group)), you could say:
var filteredFooDetails = Model.FooDetails.Where(x => x.Group == group));
#Html.EditorFor(f => filteredFooDetails);
EDIT
As a comment mentioned, this will break model binding. To be able to get the value out on the other end, your controller method would have to accept both a model, and another argument for filteredFooDetails. Using the property on the model object might lead to confusion on another developers part. How would they know that FooDetails on this particular model object may only be a subset of the whole?
On the other hand, if none of that matters, you can also just make the variable name fooDetails instead of filteredFooDetails and model binding will pick it up.
I looked at this question while trying to solve a similar problem. I needed to create a grid ordered by categories while filtering the main model collection by category to appear under each category heading. I created an editor template for the entire view model and call Html.DisplayForModel() from the view.
Page view model
namespace GoodHousekeeping.MVC.Models
{
public class ViewIngredientPageModel
{
public IEnumerable<ViewIngredientModel> ViewIngredientModels { get; set; }
public IEnumerable<ViewIngredientCategoryModel>
ViewIngredientCategoryModels { get; set; }
}
}
View Model
namespace GoodHousekeeping.MVC.Models
{
public class ViewIngredientModel
{
public int? IngredientId { get; set; }
[DisplayName("Ingredient Name")]
public string Name { get; set; }
public int IngredientCategoryId { get; set; }
#region navigation
public ViewIngredientCategoryModel IngredientCategory { get; set; }
#endregion
}
}
Main view
#model GoodHousekeeping.MVC.Models.ViewIngredientPageModel
#Html.DisplayForModel()
EditorTemplate - this is where we're working on the whole view model and doing the filtering. The file name is ViewIngredientPageModel.cshtml in shared/editortemplates folder. I'm calling a standard editorfor template for each item to appear under the category headers.
#model GoodHousekeeping.MVC.Models.ViewIngredientPageModel
#foreach (var category in Model.ViewIngredientCategoryModels)
{
<p>#category.IngredientCategoryName</p>
var category1 = category;
var viewIngredientModels = (from i in Model.ViewIngredientModels
where i.IngredientCategoryId == category1.IngredientCategoryId
select i);
#Html.DisplayFor(m => viewIngredientModels)
}

Categories