PartialView or enumerate using foreach in View Model for an inner class using Entity Framework? - linq

I am creating (my first) real simple ASP.NET MVC blog site where I am wondering what is the best approach to sort my comments by the the latest DateTime and how to go about injecting the query results using LINQ into the View or do I need an IQueryable<T> PartialView?
I have been reading up on IEnumerable<T> vs IQueryable<T>, and I would like to think that I wouldn't want the comments in-memory until I have them filtered and sorted.
I was thinking a PartialView using #model Queryable<Models.Blogsite.Comment> where I pass the inner class to the [ChildAction] using the ViewBag or can I just use a foreach loop in the View?
My Article class looks a little like this:
public class Article
{
//...more properties
public virtual IQueryable<Comment> Comments { get; set; }
}
My Comment class looks a little like this:
public class Comment
{
[Key]
public int CommentId { get; set; }
[ForeignKey("Article")]
public int ArticleId { get; set; }
[DataType(DataType.DateTime), Timestamp,ScaffoldColumn(false)]
public DateTime Timestamp { get; set; }
//...more properties
public virtual Article Article { get; set; }
}
And then if I did implement a PartialView it would look a little like this:
#model IQueryable<BlogSite.Models.Comment>
<table>
<tbody>
#foreach (BlogSite.Models.Comment comment in ViewBag.Comments)
{
<tr>
<td>
#...
I changed my public class Article to this:
public class Article
{
//...more properties
public virtual ICollection<Comment> Comments { get; set; }
}
And then I created a method in my ArticleRepository where I call it from UnitOfWOrk which is instantiated within the controller:
public IEnumerable<Comment> GetCommentsByArticleId(int id)
{
List<Comment> tempList = new List<Comment>();
var article = GetArticleById(id);
var comments = article.Comments.Where(c => c.ParentCommentId == null).OrderByDescending(c => c.Timestamp);
foreach (var c in comments)
{
if (c.isRoot)
{
tempList.Add(c);
// Replies
foreach (var r in article.Comments.Where(x => x.ParentCommentId == c.CommentId).OrderBy(x => x.Timestamp))
{
tempList.Add(r);
}
}
}
return tempList;
}
I have a property called bool isRoot in my comment class so I can render all comments from the property int ParentCommentId so users can respond to these comments. My question now is that if my web application has thousands of articles and the querying is done in-memory and not at the database will this eventually turn into a performance issue?
Won't the temporary list go into garbage collection once it is out of scope? What is the advantage of using IQueryable in this scenario?

Using ViewBag is a bad practice. If you need a sorted list - do it in a controller:
var comments = context.GetComments().ToList();
comments.Sort((x, y) => y.Timestamp.CompareTo(x.Timestamp));
return View(comments)
And pass a sorted list in the view:
#model IEnumerable<BlogSite.Models.Comment>
<table>
....

Related

How to keep a collection of items for dropdown list in MVC model?

to keep things simple, I have a model Survey with the following properties:
class SurveyItem {
public string Question { get; set; }
public string SelectedAnswerCode { get; set; }
public List<Answer> Answers { get; set; }
}
where Answer is like:
class Answer {
public int AnswerCode { get; set; }
public string AnswerText { get; set; }
}
Answers is used to build a dropdown listbox of possible answers for (a user selects one)
In my View I use a Model of IEnumerable
where for each question I have a list of answers to choose from.
I prefill this collection and pass to my View. When I click submit, it goes back to the controller for validation. If the model is not valid, I pass it to the same View for a user to fix his answers, like usual.
Question - Answers collection used for dropdown list is not preserved in the model when I submit. I use HiddenFor, EditorFor and DropDownListFor for single value properties, but, how do I keep a collection of possible answers in the Model?
P.S>
Thanks.
P.S. I am using single line code #Html.DropDownListFor to render the dropdown in my EditorTemplate:
#Html.DropDownListFor(model => model.SelectedAnswerCode,
new SelectList(Model.Answers, "AnswerCode", "AnswerText", 0))
You'll need to add virtual to the Answers declaration.
class SurveyItem {
public string Question { get; set; }
public string SelectedAnswerCode { get; set; }
public virtual List<Answer> Answers { get; set; }
}
This seems to do the trick: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Basically in your view do something like this:
#for(int i = 0; i < Model.Answers.Count; i++)
{
#Html.Hidden(string.Format("Answers[{0}].AnswerCode", i), Model.Answers[i].AnswerCode)
#Html.Hidden(string.Format("Answers[{0}].AnswerText", i), Model.Answers[i].AnswerText)
#Html.RadioButton("SelectedAnswerCode", Model.Answers[i].AnswerCode)
#Model.Answers[i].AnswerText
}
EDIT:
Alternatively, you can create your own HtmlHelper extension. For example:
public static class CustomHtmlHelperExtensions
{
public static MvcHtmlString HiddenForSurveyAnswers(this HtmlHelper htmlHelper, IEnumerable<Models.Answer> answers)
{
var html = new StringBuilder();
int index = 0;
foreach (var answer in answers)
{
html.AppendLine(htmlHelper.Hidden(string.Format("Answers[{0}].AnswerCode", index), answer.AnswerCode).ToString());
html.AppendLine(htmlHelper.Hidden(string.Format("Answers[{0}].AnswerText", index), answer.AnswerText).ToString());
index++;
}
return MvcHtmlString.Create(html.ToString());
}
}
Then add an #using YourMvcApplicationNamespace to the top of the view and then use the extension like:
#Html.HiddenForSurveyAnswers(Model.Answers)
With MVC, you can write an Editor Template to preserve the Model.Answers as well :)
Save a view named Answer.chtml in \Views\Shared\EditorTemplates.
Add the following code:
#model Answer
#Html.HiddenFor(item => item.AnswerCode)
#Html.HiddenFor(item => item.AnswerText)
Then in you original view, add it:
#Html.EditorFor(model => model.Answers)
#Html.DropDownListFor(model => model.SelectedAnswerCode, new SelectList(Model.Answers, "AnswerCode", "AnswerText", 0))
In this manner you don't have to worry about writing foreach statements or worry about their ids.
Hope it helps.

implementing dropdownlist in asp.net mvc 3

I am teaching myself asp .net mvc3. I have researched a lot but the more I read the more confused I become. I want to create a page where users can register their property for sale or rent.
I have created a database which looks like this:
public class Property
{
public int PropertyId { get; set; }
public int PropertyType { get; set; }
ยทยทยท
public int Furnished { get; set; }
...
}
Now, I want dropdownlistfor = PropertyType and Furnished.
Property type would be
1 Flat
2 House
3 Detached House
...
Furnished would be:
1 Furnished
2 UnFurnished
3 PartFurnished
...
Now, I am really not sure where to keep this information in my code. Should I have 2 tables in my database which store this lookup? Or should I have 1 table which has all lookups? Or should I just keep this information in the model?
How will the model bind to PropertyType and Furnished in the Property entity?
Thanks!
By storing property types and furnished types in the database, you could enforce data integrity with a foreign key, rather than just storing an integer id, so I would definitely recommend this.
It also means it is future proofed for if you want to add new types. I know the values don't change often/will never change but if you wanted to add bungalow/maisonette in the future you don't have to rebuild and deploy your project, you can simply add a new row in the database.
In terms of how this would work, I'd recommend using a ViewModel that gets passed to the view, rather than passing the database model directly. That way you separate your database model from the view, and the view only sees what it needs to. It also means your drop down lists etc are strongly typed and are directly in your view model rather than just thrown into the ViewBag. Your view model could look like:
public class PropertyViewModel
{
public int PropertyId { get; set; }
public int PropertyType { get; set; }
public IEnumerable<SelectListItem> PropertyTypes { get; set; }
public int Furnished { get; set; }
public IEnumerable<SelectListItem> FurnishedTypes { get; set; }
}
So then your controller action would look like:
public class PropertiesController : Controller
{
[HttpGet]
public ViewResult Edit(int id)
{
Property property = db.Properties.Single(p => p.Id == id);
PropertyViewModel viewModel = new PropertyViewModel
{
PropertyId = property.Id,
PropertyType = property.PropertyType,
PropertyTypes = from p in db.PropertyTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.PropertyTypeId.ToString()
}
Furnished = property.Furnished,
FurnishedTypes = from p in db.FurnishedTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.FurnishedTypeId.ToString()
}
};
return View();
}
[HttpGet]
public ViewResult Edit(int id, PropertyViewModel propertyViewModel)
{
if(ModelState.IsValid)
{
// TODO: Store stuff in the database here
}
// TODO: Repopulate the view model drop lists here e.g.:
propertyViewModel.FurnishedTypes = from p in db.FurnishedTypes
orderby p.TypeName
select new SelectListItem
{
Text = p.TypeName,
Value = g.FurnishedTypeId.ToString()
};
return View(propertyViewModel);
}
}
And your view would have things like:
#Html.LabelFor(m => m.PropertyType)
#Html.DropDownListFor(m => m.PropertyType, Model.PropertyTypes)
I usually handle this sort of situation by using an enumeration in code:
public enum PropertyType {
Flat = 1,
House = 2,
Detached House = 3
}
Then in your view:
<select>
#foreach(var val in Enum.GetNames(typeof(PropertyType)){
<option>val</option>
}
</select>
You can set the id of the option equal to the value of each item in the enum, and pass it to the controller.
EDIT: To directly answer your questions:
You can store them as lookups in the db, but for small unlikely to change things, I usually just use an enum, and save a round trip.
Also look at this approach, as it looks better than mine:
Converting HTML.EditorFor into a drop down (html.dropdownfor?)

Trying to Extend ProDinner Chef class with collection of Phone Numbers

I am trying to extend ProDinner by adding phone numbers to Chef.
ChefInput view model:
public class ChefInput :Input
{
public string Name { get; set; }
public ChefInput()
{
PhoneNumberInputs = new List<PhoneNumberInput>(){
new PhoneNumberInput()
};}
public IList<PhoneNumberInput> PhoneNumberInputs { get; set; }
}
PhoneInput view model:
public class PhoneNumberInput :Input
{
public string Number { get; set; }
public PhoneType PhoneType { get; set; } <-- an enum in Core project
}
Chef Create.cshtml file:
#using (Html.BeginForm())
{
#Html.TextBoxFor(o => o.Name)
#Html.EditorFor(o => o.PhoneNumberInputs)
}
PhoneNumberInput.cshtml in EditorTemplate folder:
#using (Html.BeginCollectionItem("PhoneNumberInputs"))
{
#Html.DropDownListFor(m => m, new SelectList(Enum.GetNames(typeof(PreDefPhoneType))))
#Html.TextBoxFor(m => m.Number)
}
When debugging and I stop it at Create in Crudere file, the Phone collection is null.
Anyone have any ideas?
Thanks in Advance.
Joe,
You don't show your controller logic but I've got a feeling you're getting null because you're not populating the PhoneNumberInputs ViewModel. From what I can see, all you're doing is newing up the list in the model. Ensure that you fill this 'list' in your controller from the database (with the appropriate values) and i'm certain all will work as planned.
[edit] - in answer to comment. don't know what the prodinner controllers etc look like but something alsong these lines:
public ActionResult Edit(int id)
{
var viewModel = new ChefInput();
viewModel.ChefInput = _context.GetById<ChefModel>(id);
viewModel.PhoneNumberInputs = _context.All<PhoneNumberInput>();
return View(viewModel);
}
as i said, not sure of the prodinner setup, but this is what i meant.

Two render bodies in layout page?

I understand that only 1 RenderBody can exist in the MVC3 layout page however I want to attempt to create another. Maybe I'm looking at it the wrong way... Ideally I want to add a testimonial section that pulls in from the DB and display 1 testimonial at a time and a different 1 for each page refresh or new page. What is the best way to go about this?
Controller
CategoryDBContext db = new CategoryDBContext();
public ActionResult Testimonial(int id)
{
TestimonialModel model = db.Testimonials.Find(id);
return View(model);
}
Model
public class TestimonialModel
{
public int ID { get; set; }
public int CategoryID { get; set; }
public string Data { get; set; }
}
public class CategoryDBContext : DbContext
{
public DbSet<TestimonialModel> Testimonials { get; set; }
}
The View is in a folder called CategoryData.
You need to be use:
Layout:
#RenderSection("Testimonial", false) #*false means that this section is not required*#
and in you View
#section Testimonial{
}
I would use #Html.Action()
Here is a great blog post about using them: https://www.c-sharpcorner.com/article/html-action-and-html-renderaction-in-Asp-Net-mvc/
This would allow you to have a TestimonialController that can take in values, query for data and return a partial view.

Returning a specified type from a method with EF

How can I return return a collection in a method from a LINQ query that has a one to many relationship?
For instance, I have the following code where I can have many Projects to a TimeTracking object. Will the type that I have defined for the return type (IEnumerable) work? It is set up in my EF model as this specific kind of relationship.
public IEnumerable<TimeTracking> GetTimeTrackings()
{
YeagerTechEntities DbContext = new YeagerTechEntities();
DbContext.Configuration.ProxyCreationEnabled = false;
DbContext.Configuration.LazyLoadingEnabled = false;
var timeTrackings = (from timeTrackingProjects in DbContext.TimeTrackings.Include("TimeTrackings.Projects")
select timeTrackingProjects).Where(p => p.TimeTrackingID > 0);
CloseConnection(DbContext);
return timeTrackings;
}
If so, when I display it in my MVC 3 View, and my View contains an IEnumerable<YeagerTech.YeagerTechWcfService.TimeTracking> model, will the model variable have records in it for the TimeTracking and Project objects? I don't think it will. My TimeTracking object is set up as follows unless I need to inherit the Project class with it (which would then have the Project properties):
public partial class TimeTracking
{
[DataMember]
public int TimeTrackingID { get; set; }
[DataMember]
public short ProjectID { get; set; }
[DataMember]
public byte[] Attachment { get; set; }
[Required]
[DataType(DataType.Date)]
[DataMember]
public System.DateTime StartDate { get; set; }
[Required]
[DataType(DataType.Date)]
[DataMember]
public System.DateTime EndDate { get; set; }
[DataMember]
public string Notes { get; set; }
[DataMember]
public System.DateTime CreatedDate { get; set; }
[DataMember]
public Nullable<System.DateTime> UpdatedDate { get; set; }
[DataMember]
public virtual Project Project { get; set; }
}
I also want my View to display the Project text that is associated with the TimeTracking and not the Project value. How can I do this?Can someone please help?
I got the following msg from invoking a method on my WCF client.
'cannot be serialized if reference tracking is disabled'
After getting the msg, I then modified my DataContracts to include references ([DataContract(IsReference = true)]).
namespace YeagerTechModel
{
[Serializable]
[DataContract(IsReference = true)]
public partial class Customer
{
public Customer()
{
this.Projects = new HashSet<Project>();
}
[DataMember]
public short CustomerID { get; set; }
[Required]
[StringLength(50)]
[DataType(DataType.EmailAddress)]
[DataMember]
public string Email { get; set; }
I am executing the following server side code to successfully get data from my database in a parent/child relationship. The Include method explicity invokes getting the related Project data for the specific Customer. I had to do it this way because you must turn LazyLoading off if you want to get your parent/child data across the wire.
If I look at the WCF messagelog, I can see the actual data coming across in a Customer object and it has the Project object inside of the Customer object.
However, after the call is made and I actually inspect the contents of the "customer" variable, I don't see any refernces to any Project data.
public IEnumerable<Customer> GetCustomers()
{
YeagerTechEntities DbContext = new YeagerTechEntities();
DbContext.Configuration.ProxyCreationEnabled = false;
DbContext.Configuration.LazyLoadingEnabled = false;
IQueryable<Customer> customer = DbContext.Customers.Include("Projects").Where(p => p.CustomerID > 0);
CloseConnection(DbContext);
return customer;
}
The thing I want to do now, is reference the Project data coming back from the call. However, I don't get any Customer object intellisense after typing "customer.". It's all pertains to an IQueryable object.
I'm passing it back into my MVC Controller as the following type:
IEnumerable<YeagerTechWcfService.Customer> customerList = db.GetCustomers();
and into my View as the following model:
IEnumerable<YeagerTech.YeagerTechWcfService.Customer>
Now, the big question is "How can I reference the Project data coming back in my View?
The below is my code for the View, but there is no intellisense for "item.Project". Note that "Email" is a property inside my Customer object.
foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Email)
</td>
Looks like your Linq query should be closer to this (NOTE: did not test the query, might need tweaking):
var query = (from tt in DbContext.TimeTrackings.Include("Projects")
where tt.TimeTrackingID > 0
select tt).ToList();
Linq query as you have written is deferred execution, you are closing your connection before retrieving the data, so that would probably cause a runtime error.
.Include() statement should specify the property on the entity (TimeTracking in your case) that need to be loaded, so in this case that would be Project property
Once you have retrieved your enumerable collection of TimeTracking entities you can access the properties of the Project entity associated with a particular TimeTracking entity like so:
foreach(var tracking in GetTimeTrackings())
{
foreach(var project in tracking.Projects)
{
// Assuming your Project entity has a Name property
Response.Write(project.Name);
}
}
I'm not sure what you mean by
I also want my View to display the Project text that is associated
with the TimeTracking and not the Project value.
can you clarify what property from which entity you want to see? What is the Project Entity definition?
In response to your comment about closing connection after retrieving the data:
The statement IQueryable<Customer> customer = DbContext.Customers.Include("Projects").Where(p => p.CustomerID > 0); does not actually execute a query against the database until you start to iterate it (most likely in your view with a foreach statement). If you add a .ToList() at the end of that statement, it will execute it and return a List<Customer> (which is also IEnumerable) which contains all the records that are result of your query.
When you try to type customer. to get intellisense for the Customer entity, you're not seeing it because customer is a list of Customer entities (or rather an IQueryiable of them) so you would need to do something like customer[0]. to access the properties of the first Customer entity in that list (or iterate over it).
I'm not 100% sure how entity references come through in ASP.NET MVC on a model entity but a really simple way you can get this done is create a model class you want to use in your view, say something like this:
public class TimeTrackingModel {
public int TimeTrackingID { get; set; }
public string ProjectName { get; set; }
}
then in your query do this:
var customers = (from tt in DbContext.TimeTrackings.Include("Projects")
where tt.TimeTrackingID > 0
select new TimeTrackingModel { TimeTrackingID = tt.TimeTrackingID, ProjectName = tt.Project.ProjectName }).ToList();
then in your view specify IEnumberable<TimeTrackingModel> as the model and then access the properties like this:
foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.ProjectName)
</td>
Actually, after further review, I can now see the Project collection in my Customer collection all the way back up to my client after adding a QucikWatch on the object.
The correct answer is the last part of my post where the LazyLoadingEnabled = false appears.

Resources