Spark-like syntax in Razor foreach - asp.net-mvc-3

We've been using Spark view engine for a while now in our application. Since the release of Resharper 6 with its great Razor support, we've caved and started to swap over.
One of the best parts of Spark was its automatic generation of variables inside a foreach loop. For instance, inside a loop you automatically get index, last, and first variables, which let you know the index, whether the item is the first item, and whether the item is the last item respectively.
My question is as follows: is there a way to have a foreach automatically generate these variables for me, so I don't have to do it manually? Do I need to create my own helper that does this?

As BuildStarted suggested, I used Phil Haack's post to formulate a solution.
public static HelperResult Each<TItem>(this IEnumerable<TItem> items, Func<EnumeratedItem<TItem>, HelperResult> template)
{
return new HelperResult(writer =>
{
int index = 0;
ICollection<TItem> list = items.ToList();
foreach (var item in list)
{
var result = template(new EnumeratedItem<TItem>(index, index == 0, index == list.Count - 1, item));
index++;
result.WriteTo(writer);
}
});
}
And the EnumeratedItem class:
public class EnumeratedItem<TModel>
{
public EnumeratedItem(int index, bool isFirst, bool isLast, TModel item)
{
this.Index = index;
this.IsFirst = isFirst;
this.IsLast = isLast;
this.Item = item;
}
public int Index { get; private set; }
public bool IsFirst { get; private set; }
public bool IsLast { get; private set; }
public TModel Item { get; private set; }
}
I was forced to convert the IEnumerable to an ICollection in order to have the Count property.
Usage:
#Model.History.Each(
#<text>
<li class="#(#item.IsLast ? "last" : string.Empty)">
#{ var versionNo = Model.History.Count - #item.Index; }
<div class="clearfix version selected">
<div class="index">
#versionNo
</div>
<div class="modified">
#item.Item.DateModified<br/>
#Html.Raw(item.Item.ModifiedBy)
</div>
<div class="view">
View changes
</div>
</div>
</li>
</text>)

Phil Haack wrote a nice example that will give you the items you're looking for here http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx
It's not the same as foreach however you'll be required to do something similar if you want the features you're looking for.
HugoWare also had a great article for IEnumerable extensions that provides the entire functionality you're looking for http://hugoware.net/blog/build-a-smarter-loop-with-c
I would probably recommend using Hugoware's example as it's more than just razor.

in Visual Studio 2010 there are "code snippets" - code parts, which allow to generate code. with some utilityes you can edit default snippet for "foreach" and write own code, with variables you need.

Related

MVC How to pass a list of objects with List Items POST action method

I want to post a List of items to controller from Razor view , but i am getting a List of objects as null
My class structre is
Model:
List<Subjects> modelItem
class Subjects
{
int SubId{get;set;}
string Name{get;set;}
List<Students> StudentEntires{get;set;}
}
class StudentEntires
{
int StudId{get;set;}
string Name{get;set;}
int Mark{get;set;}
}
The model itself is a list of items and every items contain List of child items as well. Example model is a list of Subjects and every subject contains a List of Students, and i want to input mark for every student
My View is like
#model IList<Subjects>
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
if (Model.Count > 0)
{
#for (int item = 0; item < Model.Count(); item++)
{
<b>#Model[item].Name</b><br />
#foreach (StudentEntires markItem in Model[item].StudentEntires)
{
#Html.TextBoxFor(modelItem => markItem.Mark)
}
}
<p style="text-align:center">
<input type="submit" class="btn btn-primary" value="Update" />
</p>
}
}
And in controller
[HttpPost]
public ActionResult OptionalMarks(int Id,ICollection<Subjects> model)
{
//BUt my model is null. Any idea about this?
}
You're finding this difficult because you're not utilising the full power of the MVC framework, so allow me to provide a working example.
First up, let's create a view model to encapsulate your view's data requirements:
public class SubjectGradesViewModel
{
public SubjectGradesViewModel()
{
Subjects = new List<Subject>();
}
public List<Subject> Subjects { get; set; }
}
Next, create a class to represent your subject model:
public class Subject
{
public int Id { get; set; }
public string Name { get; set; }
public List<Student> StudentEntries { get; set; }
}
Finally, a class to represent a student:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Grade { get; set; }
}
At this point, you have all the classes you need to represent your data. Now let's create two controller actions, including some sample data so you can see how this works:
public ActionResult Index()
{
var model = new SubjectGradesViewModel();
// This sample data would normally be fetched
// from your database
var compsci = new Subject
{
Id = 1,
Name = "Computer Science",
StudentEntries = new List<Student>()
{
new Student { Id = 1, Name = "CompSci 1" },
new Student { Id = 2, Name = "CompSci 2" },
}
};
var maths = new Subject
{
Id = 2,
Name = "Mathematics",
StudentEntries = new List<Student>()
{
new Student { Id = 3, Name = "Maths 1" },
new Student { Id = 4, Name = "Maths 2" },
}
};
model.Subjects.Add(compsci);
model.Subjects.Add(maths);
return View(model);
}
[HttpPost]
public ActionResult Index(SubjectGradesViewModel model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Success");
}
// There were validation errors
// so redisplay the form
return View(model);
}
Now it's time to construct the views, and this part is particularly important when it comes to sending data back to a controller. First up is the Index view:
#model SubjectGradesViewModel
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
#Html.EditorFor(m => m.Subjects) <br />
<input type="submit" />
}
You'll notice I'm simply using Html.EditorFor, whilst passing Subjects as the parameter. The reason I'm doing this is because we're going to create an EditorTemplate to represent a Subject. I'll explain more later on. For now, just know that EditorTemplates and DisplayTemplates are special folder names in MVC, so their names, and locations, are important.
We're actually going to create two templates: one for Subject and one for Student. To do that, follow these steps:
Create an EditorTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\EditorTemplates).
Create a strongly-typed view in that directory with the name that matches your model (i.e. in this case you would make two views, which would be called Subject.cshtml and Student.cshtml, respectively (again, the naming is important)).
Subject.cshtml should look like this:
#model Subject
<b>#Model.Name</b><br />
#Html.HiddenFor(m => m.Id)
#Html.HiddenFor(m => m.Name)
#Html.EditorFor(m => m.StudentEntries)
Student.cshtml should look like this:
#model Student
#Html.HiddenFor(m => m.Id)
#Html.HiddenFor(m => m.Name)
#Html.DisplayFor(m => m.Name): #Html.EditorFor(m => m.Grade)
<br />
That's it. If you now build and run this application, putting a breakpoint on the POST index action, you'll see the model is correctly populated.
So, what are EditorTemplates, and their counterparts, DisplayTemplates? They allow you to create reusable portions of views, allowing you to organise your views a little more.
The great thing about them is the templated helpers, that is Html.EditorFor and Html.DisplayFor, are smart enough to know when they're dealing with a template for a collection. That means you no longer have to loop over the items, manually invoking a template each time. You also don't have to perform any null or Count() checking, because the helpers will handle that all for you. You're left with views which are clean and free of logic.
EditorTemplates also generate appropriate names when you want to POST collections to a controller action. That makes model binding to a list much, much simpler than generating those names yourself. There are times where you'd still have to do that, but this is not one of them.
Change the action method signature to
public ActionResult OptionalMarks(ICollection<Subjects> model)
Since in your HTML, it does not look like there is anything named Id in there. This isn't your main issue though.
Next, do the following with the foor loop
#for(int idx = 0; idx < Model[item].StudentEntires.Count();idx++)
{
#Html.TextBoxFor(_ => Model[item].StudentEntries[idx])
}
Possibly due to the use of a foreach loop for the StudentEntries, the model binder is having trouble piecing everything together, and thus a NULL is returned.
EDIT:
Here's an example:
Controller
public class HomeController : Controller
{
public ActionResult Index()
{
var viewModel = new IndexViewModel();
var subjects = new List<Subject>();
var subject1 = new Subject();
subject1.Name = "History";
subject1.StudentEntires.Add(new Student { Mark = 50 });
subjects.Add(subject1);
viewModel.Subjects = subjects;
return View(viewModel);
}
[HttpPost]
public ActionResult Index(IndexViewModel viewModel)
{
return new EmptyResult();
}
}
View
#model SOWorkbench.Controllers.IndexViewModel
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
if (Model.Subjects.Any())
{
int subjectsCount = Model.Subjects.Count();
for (int item = 0; item < subjectsCount; item++)
{
<b>#Model.Subjects[item].Name</b><br />
int studentEntriesCount = Model.Subjects[item].StudentEntires.Count();
for(int idx = 0;idx < studentEntriesCount;idx++)
{
#Html.TextBoxFor(_ => Model.Subjects[item].StudentEntires[idx].Mark);
}
}
<p style="text-align:center">
<input type="submit" class="btn btn-primary" value="Update" />
</p>
}
}
When you post the form, you should see the data come back in the viewModel object.

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

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>
....

MVC3 Persisting data from controller to view back to controller

I have a model containing a couple of lists:
[Display(Name = "Facilities")]
public List<facility> Facilities { get; set; }
[Display(Name = "Accreditations")]
public List<accreditation> Accreditations { get; set; }
I populate these lists initially from my controller:
public ActionResult Register()
{
var viewModel = new RegisterModel();
viewModel.Facilities = m_DBModel.facilities.ToList();
viewModel.Accreditations = m_DBModel.accreditations.ToList();
return View(viewModel);
}
When they get to my view they are populated with the DB records (great). I then pass the model to the partial view which displays these lists as checkboxes, ready for user manipulation (I have tried based on another suggestion using for loop instead of foreach loop, made no difference):
#model LanguageSchoolsUK.Models.RegisterModel
#foreach (var item in Model.Facilities)
{
#Html.Label(item.name);
#Html.CheckBox(item.name, false, new { id = item.facility_id, #class = "RightSpacing", #description = item.description })
}
When I submit the form and it ends up back at my controller this time calling the overloaded register function on the controller:
[HttpPost]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Do stuff
}
return View(model);
}
The problem is that the model parameter containing the lists (Facilities and Accreditations) is telling me that the lists are null.
Please can somebody tell me what I am doing wrong, why aren't they populated with the collections that I originally passed through and hopefully a way of asking whick ones have been checked?
Thanks.
I have tried based on another suggestion using for loop instead of
foreach loop, made no difference
Try again, I am sure you will have more luck this time. Oh and use strongly typed helpers:
#model LanguageSchoolsUK.Models.RegisterModel
#for (var i = 0; i < Model.Facilities.Count; i++)
{
#Html.HiddenFor(x => x.Facilities[i].name)
#Html.LabelFor(x => x.Facilities[i].IsChecked, Model.Facilities[i].name);
#Html.CheckBoxFor(
x => x.Facilities[i].IsChecked,
new {
id = item.facility_id,
#class = "RightSpacing",
description = item.description // <!-- HUH, description attribute????
}
)
}
Also you will undoubtedly notice from my answer that checkboxes work with boolean fields on your model, not integers, not decimals, not strings => BOOLEANS.
So make sure that you have a boolean field on your model which will hold the state of the checkbox. In my example this field is called IsChecked but obviously you could feel absolutely free to find it a better name.

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.

MVC3 error when passing a model from controller to view while using a viewModel

I'm sorry, for I'm sure there's a way to do this with a viewModel, however I'm very inexperienced with this and don't even know if I'm doing it correctly.
What I'm trying to do is pass multiple blogs and the profile info of the user who posted each blog to a view.
I'm getting the following error.
The model item passed into the dictionary is of type
'ACapture.Models.ViewModels.BlogViewModel', but this dictionary
requires a model item of type
'System.Collections.Generic.IEnumerable`1[ACapture.Models.ViewModels.BlogViewModel]'.
I'm trying to pass the following query results to the view.
var results = (from r in db.Blog.AsEnumerable()
join a in db.Profile on r.AccountID equals a.AccountID
select new { r, a });
return View(new BlogViewModel(results.ToList()));
}
This is my viewModel
public class BlogViewModel
{
private object p;
public BlogViewModel(object p)
{
this.p = p;
}
}
And my view
#model IEnumerable<ACapture.Models.ViewModels.BlogViewModel>
#{
ViewBag.Title = "Home Page";
}
<div class="Forum">
<p>The Forum</p>
#foreach (var item in Model)
{
<div class="ForumChild">
<img src="#item.image.img_path" alt="Not Found" />
<br />
<table>
#foreach (var comment in item.comment)
{
<tr><td></td><td>#comment.Commentation</td></tr>
}
</table>
</div>
}
</div>
Thanks in advance.
I guess you need to change your view model a little to:
public class BlogViewModel
{
public Blog Blog { get; set; }
public Profile Profile{ get; set; }
}
and then return it as follow:
var results = (from r in db.Blog.AsEnumerable()
join a in db.Profile on r.AccountID equals a.AccountID
select new new BlogViewModel { Blog = r, Profile = a });
return View(results.ToList());
Then in your foreach loop inside of view, you will get an objects that will contain both - profile and blog info, so you can use it like f.e. #item.Profile.Username
I'm not entirely sure what you're trying to accomplish with the ViewModel in this case, but it seems like you are expecting for the page to represent a single blog with a collection of comments. In this case you should replace
IEnumerable<ACapture.Models.ViewModels.BlogViewModel>
With
ACapture.Models.ViewModels.BlogViewModel
Then Model represents a single BlogViewModel, that you can iterate over the comments by using Model.comments and access the image using Model.image.img_path.
If this not the case, and you intend to have multiple BlogViewModels per page, then you will have to actually construct a collection of BlogViewModels and pass that to the view instead.

Resources