I have a simple ASP.NET MVC application which has possibility to search, filter and page a list of vehicle makes. I implemented sorting, filtering and paging on controller (on "Index" action) following the tutorials from kudvenkat on youtube https://www.youtube.com/watch?v=srN56uxw76s
So, this is my "VehicleMakeController" and "Index" action:
public class VehicleMakeController : Controller
{
private readonly IVehicleRepository _vehicleRepository;
public VehicleMakeController()
{
_vehicleRepository = new VehicleRepository(new VehicleDbContext());
}
// GET: VehicleMake
public ActionResult Index(string search, int? page, string sort)
{
ViewBag.SortNameParameter = string.IsNullOrEmpty(sort) ? "Name desc" : "";
var makes = _vehicleRepository.AllMakes;
switch (sort)
{
case "Name desc":
makes = makes.OrderByDescending(x => x.Name);
break;
default:
makes = makes.OrderBy(x => x.Name);
break;
}
if (search == null)
{
return View(makes.ToList().ToPagedList(page ?? 1, 5));
}
return View(makes.Where(x => x.Name.ToLower().StartsWith(search)).ToList().ToPagedList(page ?? 1, 5));
}
and this is my "Index" view:
#using PagedList;
#using PagedList.Mvc;
#model IPagedList<Project.Service.Entities.VehicleMake>
#{
ViewBag.Title = "Vehicle Makes";
}
<h2>#ViewBag.Title</h2>
#Html.ActionLink("Create", "CreateVehicleMake")
<br/>
<br/>
#using (#Html.BeginForm("Index", "VehicleMake", FormMethod.Get))
{
<p>
#Html.TextBox("search") <input type="submit" value="Search"/>
</p>
}
<table class="table">
<thead>
<tr>
<th>#Html.ActionLink("Name", "Index", new { sort = ViewBag.SortNameParameter, search = Request.QueryString["search"] })</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var vehicleMake in Model)
{
<tr>
<td>#vehicleMake.Name</td>
<td>#Html.ActionLink("Edit", "EditVehicleMake", new {id = vehicleMake.Id})</td>
<td>#Html.ActionLink("Delete", "DeleteVehicleMake", new {id = vehicleMake.Id})</td>
</tr>
}
</tbody>
</table>
#Html.PagedListPager(Model, page => Url.Action("Index", new { page, search = Request.QueryString["search"], sort = Request["sort"]}),
new PagedListRenderOptions() { Display = PagedListDisplayMode.IfNeeded, DisplayPageCountAndCurrentLocation = true})
#if (!Model.Any())
{
<b>No rows match search criteria!</b>
}
In application I'm using repository pattern and I have "AllMakes" method to retrieve all vehicle makes from database. So, this is my "VehicleRepository":
public class VehicleRepository : IVehicleRepository
{
private readonly VehicleDbContext _context;
public VehicleRepository(VehicleDbContext context)
{
_context = context;
}
public IEnumerable<VehicleMake> AllMakes => _context.VehicleMakes;
}
and this is my "IVehicleRepository" interface:
public interface IVehicleRepository
{
IEnumerable<VehicleMake> AllMakes { get; }
}
My DbContext class is following:
public class VehicleDbContext : DbContext
{
public VehicleDbContext() : base("VehicleDbContext")
{
}
public DbSet<VehicleMake> VehicleMakes { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
}
}
This everything works OK, but now I want to implement sorting and paging in repository and remove it from controller. The parameters should be passed trough get/query method. I'm not sure how to do this and I would appreciate any help.
You can declare a function in IVehicleRepository and implement it.
public interface IVehicleRepository
{
IEnumerable<VehicleMake> AllMakes { get; }
List<VehicleMake> GetVehicleWithPagination(string search, int? page, string sort);
}
public class VehicleRepository : IVehicleRepository
{
// your old code
public List<VehicleMake> GetVehicleWithPagination(string search, int? page, string sort)
{
// this is your code from controller
switch (sort)
{
case "Name desc":
makes = AllMakes.OrderByDescending(x => x.Name);
break;
default:
makes = AllMakes.OrderBy(x => x.Name);
break;
}
if (search == null)
{
return AllMakes.ToList().ToPagedList(page ?? 1, 5);
}
}
}
And your controller:
public ActionResult Index(string search, int? page, string sort)
{
ViewBag.SortNameParameter = string.IsNullOrEmpty(sort) ? "Name desc" : "";
return View(_vehicleRepository.GetVehicleWithPagination(search, page, sort));
}
Related
I am trying to insert selected checkbox values to database. But I get an error
Unable to cast object of type 'System.Collections.Generic.List` to type 'System.String'
Please help. Thanks.
Model:
public class CheckboxModel
{
public int Id { get; set; }
public string Name { get; set; }
public bool Checked { get; set; }
}
public class MainModel
{
public List<CheckboxModel> CheckBoxes { get; set; }
}
HomeController:
// Get
public ActionResult Index()
{
MainModel model = new MainModel();
var list = new List<CheckboxModel>
{
new CheckboxModel{Id = 1, Name = "Male", Checked = false},
new CheckboxModel{Id = 2, Name = "Female", Checked = false},
};
model.CheckBoxes = list;
return View(model);
}
[HttpPost]
public ActionResult Create([Bind(Include = "Checkboxes")] MainModel model)
{
if (ModelState.IsValid)
{
db.MainModel.Add(model);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(MainModel);
}
View:
#using (Html.BeginForm("Create", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#for (var i = 0; i < Model.Checkboxes.Count; i++)
{
<table>
<tr>
<td>
#Html.HiddenFor(m => Model.Checkboxes[i].Id)
#Html.HiddenFor(m => Model.Checkboxes[i].Name)
#Html.CheckBoxFor(m => Model.Checkboxes[i].Checked)
</td>
<td>
#Html.DisplayFor(m => Model.Checkboxes[i].Name)
</td>
</tr>
</table>
}
1) How to pass id in AddBookInCategory form in #Html.EditorFor(model=>model.CategoryID).
2) And second how after adding new item in public ActionResult AddBookInCategory(Books book) return to category with id it must be something like return RedirectToAction("ShowBooksFromCategory", 1); but it does not work.
HomeController:
public class HomeController : Controller
{
//
// GET: /Home/
TEntities TE = new TEntities();
public ActionResult Index()
{
return View();
}
public ActionResult ShowCategories()
{
return View(TE.Category.ToList());
}
public ActionResult ShowBooksFromCategory(int id)
{
return View(TE.Books.Where(key => key.CategoryID == id).ToList());
}
public ActionResult AddBookInCategory(int id)
{
return View();
}
[HttpPost]
public ActionResult AddBookInCategory(Books book)
{
TE.Books.Add(book);
TE.SaveChanges();
return View();
//return RedirectToAction("ShowBooksFromCategory", 1);
}
}
AddBookInCategory.cshtml:
#model TestDataBase.Models.Books
#{
ViewBag.Title = "AddBookInCategory";
}
<h2>AddBookInCategory</h2>
#using(#Html.BeginForm())
{
<p>Book Name:</p>
<br />
#Html.EditorFor(model=>model.BookName)
<p>Category:</p>
<br />
#Html.EditorFor(model=>model.CategoryID)
<input type="submit" value="Create"/>
}
Index.cshtml
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
#Html.ActionLink("ShowCategories", "ShowCategories")
ShowBooksFromCategory.cshtml
#model IEnumerable<TestDataBase.Models.Books>
#{
ViewBag.Title = "ShowBooksFromCategory";
}
<h2>ShowBooksFromCategory</h2>
<table>
#foreach(var item in Model)
{
<tr>
<td>
#item.BookName
</td>
</tr>
}
</table>
#Html.ActionLink("Add item", "AddBookInCategory", new {id=Model.Select(key=>key.CategoryID).FirstOrDefault() })
ShowCategories.cshtml
#model IEnumerable<TestDataBase.Models.Category>
#{
ViewBag.Title = "ShowCategories";
}
<h2>ShowCategories</h2>
<table>
#foreach(var item in Model )
{
<tr>
<td>
#item.CategoryName
</td>
<td>
#Html.ActionLink("ShowBooksFromCategory", "ShowBooksFromCategory", new { id=item.CategoryID})
</td>
</tr>
}
</table>
Models:
public partial class Books
{
public int CategoryID { get; set; }
public string BookName { get; set; }
public int BookID { get; set; }
}
public partial class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
}
Hi #A191919 To redirect with an int you can do the following (this is exactly the same thing you did within your actionlink to get the first form..)
return RedirectToAction("ShowBooksFromCategory", new { id = book.CategoryID });
To get the ID into your form you could use ViewBag.
public ActionResult AddBookInCategory(int id)
{
ViewBag.id = id;
return View();
}
The use the viewbag value to fill in your form input.
So my story is that I am having trouble with the post to the controller, the view seems to work fine. When the postback happens the tm.BookId is 0 (should be 1) and the list count is 0. First I will display the model:
public class TransferModel
{
public TransferModel()
{
cbItems = new List<CheckBoxItem>();
}
public List<CheckBoxItem> cbItems {get;set;}
public int BookId;
public class CheckBoxItem
{
public int AttributeId { get; set; }
public string Attribute { get; set; }
public bool Selected { get; set; }
}
}
The Controller part:
public ActionResult AddAttributes(int id = 0)
{
db.transMod.BookId = id;
BookInfo book = db.BookInfoes.Find(id);
var latts = db.BookAtts.ToList();
foreach (BookAtt ba in latts)
{
db.transMod.cbItems.Add(new TransferModel.CheckBoxItem { Attribute = ba.Attribute, AttributeId = ba.BookAttId, Selected = false });
}
List<BookAtt> atInList = book.BookAtts.ToList();
foreach (TransferModel.CheckBoxItem cb in db.transMod.cbItems)
{
if (atInList.Exists(item => item.Attribute == cb.Attribute))
cb.Selected = true;
}
return View(db.transMod);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddAttributes(TransferModel tm)
{
List<BookAtt> atPool = db.BookAtts.ToList();
BookInfo book = db.BookInfoes.Find(tm.BookId);
foreach (TransferModel.CheckBoxItem sel in tm.cbItems)
{
if (sel.Selected)
book.BookAtts.Add(atPool.Find(item1 => item1.Attribute == sel.Attribute));
}
db.SaveChanges();
return RedirectToAction("AddAttributes");
}`enter code here`
And finally the view:
#model BrightStar.Models.TransferModel
#{
ViewBag.Title = "Update Attributes";
}
<h2>Add Attributes</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
<table>
#Html.HiddenFor(model => Model.BookId)
#Html.HiddenFor(model => Model.cbItems)
#foreach (var itm in Model.cbItems)
{
<tr>
<td>#Html.HiddenFor(mo => itm.AttributeId)</td>
<td>#Html.CheckBoxFor(mo => itm.Selected)</td>
<td>#Html.DisplayFor(mo => itm.Attribute)</td>
</tr>
}
</table>
<p>
<input type="submit" value="Save" />
</p>
}
enter code here
Model binding doesn't happen automatically, items needs to be in certain format to get binded to list properties in POST actions. Check this out.
Try checking out the value of BookId property in the DOM to confirm it is 1, otherwise it should bind normally.
You should reference your model's properties in helpers to correctly generate names for your controls:
#Html.HiddenFor(model => Model.cbItems)
should be
#Html.HiddenFor(model => model.cbItems)
I am using ViewBag to help me sort a list of students found within a list of classes. I have read that ViewBag is something that should be avoided at all costs attempting to build a proper MVC project.
When viewing the page that the below code generates, one is able to sort through a list of students in a variety of ways (First Name by alpha, Last name by alpha, date enrolled, etc), and view only a limited number of students per page.
I do not know exactly how to translate my code to use a ViewModel in place of my current design.
I am using the following code:
Model (Student):
public class Student
{
public int StudentID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public string Email { get; set; }
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
Model (Enrollment):
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public string Grade { get; set; } // pass, fail, incomplete
public virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
(I also have a Course model, but it is not referenced directly by the controller below, so I will omit it here - if it is necessary to show its details, please let me know.)
Controller:
public class StudentController : Controller
{
private SchoolContext db = new SchoolContext();
//
// GET: /Student/
public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
{
ViewBag.CurrentSort = sortOrder;
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "Name desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "Date desc" : "Date";
ViewBag.FNameSortParm = sortOrder == "FName" ? "FName desc" : "FName";
ViewBag.EmailSortParm = sortOrder == "Email" ? "Email desc" : "Email";
if (Request.HttpMethod == "GET")
{
searchString = currentFilter;
}
else
{
page = 1;
}
ViewBag.CurrentFilter = searchString;
var students = from s in db.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
switch (sortOrder)
{
case "Name desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "Date desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
case "FName":
students = students.OrderBy(s => s.FirstMidName);
break;
case "FName desc":
students = students.OrderByDescending(s => s.FirstMidName);
break;
case "Email":
students = students.OrderBy(s => s.Email);
break;
case "Email desc":
students = students.OrderByDescending(s => s.Email);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
int pageSize = 4;
int pageNumber = (page ?? 1);
return View(students.ToPagedList(pageNumber, pageSize));
}
And my view:
#model PagedList.IPagedList<MVCAppName.Models.Student>
#{
ViewBag.Title = "Students";
}
<h2>Students</h2>
<p>
#Html.ActionLink("Create New", "Create")
</p>
#using (Html.BeginForm())
{
<p>
Find by name: #Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
<input type="submit" value="Search" /></p>
}
<table>
<tr>
<th></th>
<th>
#Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm, currentFilter = ViewBag.CurrentFilter })
</th>
<th>
#Html.ActionLink("First Name", "Index", new { sortOrder = ViewBag.FNameSortParm, currentFilter = ViewBag.CurrentFilter })
</th>
<th>
#Html.ActionLink("Email", "Index", new { sortOrder = ViewBag.EmailSortParm, currentFilter = ViewBag.CurrentFilter })
</th>
<th>
#Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm, currentFilter = ViewBag.CurrentFilter })
</th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.ActionLink("Edit", "Edit", new { id=item.StudentID }) |
#Html.ActionLink("Details", "Details", new { id=item.StudentID }) |
#Html.ActionLink("Delete", "Delete", new { id=item.StudentID })
</td>
<td>
#Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
#Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
#Html.DisplayFor(modelItem => item.Email)
</td>
<td>
#Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
</tr>
}
</table>
<div>
Page #(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber)
of #Model.PageCount
#if (Model.HasPreviousPage)
{
#Html.ActionLink("<<", "Index", new { page = 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
#Html.Raw(" ");
#Html.ActionLink("< Prev", "Index", new { page = Model.PageNumber - 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
}
else
{
#:<<
#Html.Raw(" ");
#:< Prev
}
#if (Model.HasNextPage)
{
#Html.ActionLink("Next >", "Index", new { page = Model.PageNumber + 1, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
#Html.Raw(" ");
#Html.ActionLink(">>", "Index", new { page = Model.PageCount, sortOrder = ViewBag.CurrentSort, currentFilter=ViewBag.CurrentFilter })
}
else
{
#:Next >
#Html.Raw(" ")
#:>>
}
</div>
The controller should handle the sorting, the view just displays the data. You already do that, so you need only to define the view model, which preivosuly you'd put in the ViewBag
public class ShowStudentsModel
{
public string CurrentSort {get;set;}
public string NameSortParm {get;set;}
//and so on... you create a property for each property set in the ViewBag
public IEnumerable<Student> Students {get;set;}
}
Then in the view
#model ShowStudentsModel
#foreach(var item in Model.Students)
{
//html code
}
I think the nicest would be to subclass the PagedList.IPagedList<T> which you are using and add sort order there. So at the end of your controller you'd have this:
return View(students.ToPagedList(pageNumber, pageSize, sortOrder));
But if you're not willing to do that, then you could simply create a new ViewModel class to hold the PagedList (your current model) as well as the supplemental data you need (i.e. your sort order).
return View(new SortedStudents
{
Students = students.ToPagedList(pageNumber, pageSize);
SortOrder = sortOrder
});
With the SortedStudents defined like this:
public class SortedStudents
{
public PagedList.IPagedList<MVCAppName.Models.Student> Students { get; set; }
public string SortOrder { get; set; }
}
You could make a wrapper around your Students class
public class StudentWrapper
{
List<Students> studentList { get; set; }
String currentSort { get; set; }
public StudentWrapper() {
studentlist = new List<Students>();
}
In your controller, you would create a new StudentWrapper
StudentWrapper sw = new StudentWrapper();
and set the list of students:
sw.studentList = db.Students.ToList();
and the sortOrder
sw.currentSort = SortOder;
You pass this model to your View
return View(sw);
in your View, you would use the StudentWrapper
#model List<MVCAppName.Models.StudentWrapper>
I dont know how your paging works, so you would have to figure this out.
But i dont see any problems in using the ViewBag either.
I am having the difficulty to post back the new data being entered. It seems that the data sent to the view are sent back to the controller, despite changes made to the data before submit.
My code is as follows:
Controller
public class GroupRateController : Controller
{
//
// GET: /GroupRate/
public ActionResult Index()
{
GroupRateModel model = new GroupRateModel();
return View(model);
}
[HttpPost]
public ActionResult Index(GroupRateModel model)
{
model.Save(model);
return View(model);
}
}
View
#model MvcApplication1.Models.GroupRateModel
#{
View.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary()
<table>
<thead>
</thead>
<tr><th>Rate Group</th><th>Default Amount</th><th>Client Amount</th></tr>
#foreach (var item in #Model.ClientRateDetails)
{
<tr><td>#item.RateGroupName</td><td align="right">#Html.DisplayFor(m => #item.RateGroupID)</td><td>#Html.EditorFor(model => item.ClientRate)</td></tr>
}
</table>
<p> <input type ="submit" value="Save" id="submit" /></p>
}
Model
using System.ComponentModel.DataAnnotations;
namespace MvcApplication1.Models
{
public class GroupRateModel
{
public List<ClientRateDetailsModel> ClientRateDetails = new List<ClientRateDetailsModel>() ;
public string Name { get; set; }
public GroupRateModel()
{
ClientRateDetails.Add(new ClientRateDetailsModel
{
RateGroupID = 1,
RateGroupName = "Test1",
ClientRate = 100
});
ClientRateDetails.Add(new ClientRateDetailsModel
{
RateGroupID = 2,
RateGroupName = "Test2",
ClientRate = 200
});
ClientRateDetails.Add(new ClientRateDetailsModel
{
RateGroupID = 3,
RateGroupName = "Test3",
ClientRate = 300
});
}
public void Save(GroupRateModel model)
{
foreach (var item in model.ClientRateDetails)
{
//...;
}
}
}
public class ClientRateDetailsModel
{
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:00.00}", NullDisplayText = "")]
[Range(0, (double)decimal.MaxValue, ErrorMessage = "Please enter a valid rate")]
public decimal? ClientRate { get; set; }
public int? RateGroupID { get; set; }
public string RateGroupName { get; set; }
}
}
This might be because the names of your input controls don't have correct names for the model binder to be able to fetch the values correctly. Also I see that the ClientRateDetails is not a property but a field in your model which won't be bound correctly. So here's how I would suggest you to improve your code:
Start with the model:
public class GroupRateModel
{
public IEnumerable<ClientRateDetailsModel> ClientRateDetails { get; set; }
public string Name { get; set; }
public GroupRateModel()
{
// Remark: You cannot assign your ClientRateDetails collection here
// because the constructor will be called by the default model binder
// in the POST action and it will erase all values that the user
// might have entered
}
public void Save(GroupRateModel model)
{
foreach (var item in model.ClientRateDetails)
{
//...;
}
}
}
public class ClientRateDetailsModel
{
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:00.00}", NullDisplayText = "")]
[Range(0, (double)decimal.MaxValue, ErrorMessage = "Please enter a valid rate")]
public decimal? ClientRate { get; set; }
public int? RateGroupID { get; set; }
public string RateGroupName { get; set; }
}
then a controller:
public class HomeController: Controller
{
public ActionResult Index()
{
var model = new GroupRateModel();
model.ClientRateDetails = new[]
{
new ClientRateDetailsModel
{
RateGroupID = 1,
RateGroupName = "Test1",
ClientRate = 100
},
new ClientRateDetailsModel
{
RateGroupID = 2,
RateGroupName = "Test2",
ClientRate = 200
},
};
return View(model);
}
[HttpPost]
public ActionResult Index(GroupRateModel model)
{
model.Save(model);
return View(model);
}
}
and then the corresponding view:
#model MvcApplication1.Models.GroupRateModel
#{
View.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary()
<table>
<thead>
<tr>
<th>Rate Group</th>
<th>Default Amount</th>
<th>Client Amount</th>
</tr>
</thead>
#Html.EditorFor(x => x.ClientRateDetails)
</table>
<p><input type ="submit" value="Save" id="submit" /></p>
}
and then have a corresponding editor template (~/Views/Home/EditorTemplates/ClientRateDetailsModel.cshtml):
#model MvcApplication1.Models.ClientRateDetailsModel
<tr>
<!-- Make sure you include the ID as hidden field
if you want to get it back inside the POST action
-->
#Html.HiddenFor(x => x.RateGroupID)
<td>#Model.RateGroupName</td>
<td align="right">#Model.RateGroupID</td>
<td>#Html.EditorFor(x => x.ClientRate)</td>
</tr>