I'm working on my first complex MVC project. I have an existing database that I used Entity Framework 4.0 to model. The project is basically a survey tool. There are 8 tables in my viewmodel each with a few properties needed in my main survey view. Those are basically questionaire, section, question, and possible answers (to be in the form of dropdownlists) plus the intermediate connecting tables.
public class MyQuestionModel
{
public Questionaire Questionaire { get; set; }
public QuestionaireSection QuestionaireSection { get; set; }
public Section Section { get; set; }
public SectionQuestion SectionQuestion { get; set; }
public Question Question { get; set; }
public QuestionType QuestionType { get; set; }
public QuestionAnswerListCode QuestionAnswerListCode { get; set; }
public AnswerListCode AnswerListCode { get; set; }
}
My ViewMode MyQuestionModel is loaded like this:
public ActionResult Index()
{
var viewModel =
from qa in db.Questionaires
join qas in db.QuestionaireSections on qa.QuestionaireKey equals qas.QuestionaireKey
join s in db.Sections on qas.SectionKey equals s.SectionKey
join sq in db.SectionQuestions on s.SectionKey equals sq.SectionKey
join q in db.Questions on sq.QuestionKey equals q.QuestionKey
join qtc in db.QuestionTypes on q.QuestionTypeKey equals qtc.QuestionTypeKey
join qddl in db.QuestionAnswerListCodes on q.QuestionKey equals qddl.QuestionKey
join ddl in db.AnswerListCodes on qddl.AnswerListCodeKey equals ddl.AnswerListCodeKey
where qa.QuestionaireName.Equals("TAD")
select new MyQuestionModel
{
Questionaire = qa,
QuestionaireSection = qas,
Section = s,
SectionQuestion = sq,
Question = q,
QuestionType = qtc,
QuestionAnswerListCode = qddl,
AnswerListCode = ddl
};
return View(viewModel);
//var viewModel = new List<MyQuestionModel>();
//return View(viewModel);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Index(IEnumerable<MyQuestionModel> model)
{
if (ModelState.IsValid )
{
// PROCESS THE POSTED DATA HERE
return RedirectToAction("Index", "MyQuestion");
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "error message");
return View(model);
}
When constructing the view by iterating through the results using nested foreach and GroupBy and OrderBy Linq statements doesn't leave me with a model with results I can post back to the controller. I tried using for loops instead of foreach but the grouping issues are causing me problems. If I try loading the ViewModel discretely with out grouping and with just the distinct data for each table I end up with problems getting the correct types for each table and the composite ViewModel. I would guess one of these 3 ways is workable but I'm not sure which way to hang my hat and grind through getting it to work. I'm getting what I need (See Image) in the view using the nested foreach loops but I think I am breaking the model in the process as when I look at the posted model in the controller it is null. Maybe my foreach statements are not constructed properly. I can't help but think there is a more elagent way of doing this. Ultimately I think using editor templates of other partial views may be best but I need to get a prototype working I can refine later.
#model IEnumerable<eValuate_Prototype_07.Models.MyQuestionModel>
#{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
<table>
#foreach (var group in (Model.OrderBy(x => x.QuestionaireSection.DefaultSequence).GroupBy(item => item.Section.SectionName)))
{
<tr>
<th colspan="3">#group.Key</th>
</tr>
foreach (var item in group.OrderBy(x => x.SectionQuestion.DefaultSequence).GroupBy(subItem => subItem.Question.Question1).Distinct()) {
<tr>
<td> </td>
<td>#item.Key</td>
<td>
<select id="ddlAnswerListCode">
<option value="#Guid.Empty"></option>
#foreach (var ans in item.OrderBy(x => x.QuestionAnswerListCode.DefaultSequence))
{
<option value="#ans.AnswerListCode.AnswerListCodeKey">#ans.AnswerListCode.AnswerListCodeName</option>
}
</select>
</td>
</tr>
}
}
</table>
<p>
<input type="submit" value="Save" />
</p>
}
My Questions:
Which is preferred foreach, for loop, or loading the view model discretely then use foreach?
Is the way I'm using foreach breaking my model for posting?
Related
I have two tables of related data with navigation properties. When I try to update the data the parent data updates but the related data doesn't and I have a null reference exception error as well. I need help understanding why and what I can do to correct it and if my setup is correct. I've read the documentation but annoyingly, it's directed towards updating a dropdown list which isn't much help to me.
Here are my entity classes:
Car.cs
namespace MyProject.Data
{
public class Car : BaseEntity
{
public string Make { get; set; }
public string Model { get; set; }
public int Reg { get; set; }
public ICollection<Hire> Hires { get; set; }
}
}
Hire.cs
namespace MyProject.Data
{
public class Hire
{
public int HireId { get; set; }
public DateTime? HireDate { get; set; }
}
}
So, as you can see above, with navigation properties, one 'Car' can have many 'Hires'. No on to the view, here is how I display that information in my edit view, I'm not sure about enumerating over the cars, it doesn't feel like it's quite right.
#model IEnumerable<MyProject.Data.Car>
<form id="carEditForm" asp-controller="Car" asp-action="Edit" method="post" asp-antiforgery="true">
#foreach (var car in Model)
{
<!-- /Car Data -->
<input type="text" asp-for="#car.Make" name="Make" />
<input type="text" asp-for="#car.Model" name="Model" />
<!-- /Hire Data -->
#foreach (var hire in item.Hires)
{
<input type="text" asp-for="#hire.HireDate" name="HireDate" />
}
<button type="submit" class="btn btn-primary">Update</button>
</form>
Here is my edit code for updating all the date in both tables.
[HttpPost]
[AutoValidateAntiforgeryToken]
public IActionResult Edit(Car car)
{
var data = _context.Car
.AsNoTracking()
.Include(t => t.Hires)
.FirstOrDefault(i => i.Id.Equals(car.Id));
_context.Car.Update(car);
foreach (var hire in data.Hires)
{
_context.Hire.Update(hire);
}
_context.SaveChanges();
return View();
}
public IActionResult Edit(int id)
{
var data = _context.Car.Include(d => d.Hires).Where(v => v.Id == id);
return View(data);
}
When I try to update my data I get the following null reference exception error, I have a feeling it's linked to the fact the car is enumerated over, I'm not sure:
NullReferenceException: Object reference not set to an instance of an
object. #foreach (var car in Model)
The 'Car' table is updated but the 'Hire' isn't. I think my setup is just not quite right and would like to have some help to fix it.
I think u need to pass collection to view as Model in method where is "carEditForm"
Maybe such:
return View(cars);
I have a action , ViewModel that shows totaly of product grouped by ProductName. But this doesn't shows me how many in every department.
Let say I have 20 computers in It-department and 10 computers in adminstration department then my code shows my productname which is "Computers".
And Totaly withch is 30 but not How many in it-department and the same for administration.
And the same for Servers or other products.
So I'am trying to use this action to get number of products in every department. I know alrteady departemnt Id's and those department are not populate dynamicaly.
// This is in my HomeController and this is my action trying to get sum of every department
[HttpGet]
public ActionResult GetStatusOfDepartments(int ProductId, int departmentId )
{
var products = context.Inventory.Where(x => x.ProductId == ProductId && x.departmentId == departmentId).FirstOrDefault();
if(products != null)
{
return Content(products.Status.ToString());
}
else
{
return Content("No products");
}
}
And I want to call "GetStatusOfDepartments" action In this ViewModel but this givs me null. Can you please help me what is wrong
to call action in this ViewModel?
#model IEnumerable<ProductsInventory.Models.StatusModel>
<table class="table">
<tr>
<th>
Produkt name
</th>
<th>
It-Department
</th>
<th>
Adminstration
</th>
<th>
TotalProducts
</th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.ProductName)
</td>
<td>
// Here I want to call like this
#Html.Action("GetStatusOfDepartments", "Home", new { ProductId = item.ProductId, departmentId = 1 })
</td>
<td>
// The same Here I want to call like this
#Html.Action("GetStatusOfDepartments", "Home", new { ProductId = item.ProductId, departmentId = 2 })
</td>
<td>
#Html.DisplayFor(modelItem => item.Status)
</td>
</tr>
}
</table>
There are a couple of things that stand out with what you have done so far.
It's very unusual to see an ActionResult returning Content(). That means a controller is providing a raw string to a view, which is not really the point of controllers.
Currently the view has #Html.Action() requests embedded within a loop, which is a big indicator that the view is not being provided with an appropriate model.
The question's title suggests it has more to do with a database query than MVC.
At the moment, a single page load will result in many database queries, at least twice the number of product types. It is best to perform as few database queries as possible as they often have a big impact on the time it takes to load each page.
In my attempt to answer this question it became obvious that there is a relatively complex database query required to create the model. Maybe you have ended up using the approach above as a way to get past that, so I will try and answer this question without a complex database query while still adhering to the MVC design patterns (that should avoid the issues mentioned above)
Create a model
You already have a pretty good idea of what you want to display on the view. It's different enough from the database models that we should create a new model.
public class ProductsInDepartments
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int ITTotal { get; set; }
public int AdminTotal { get; set; }
public int Status { get; set; }
}
This is simply a property for each column in your view. I think you only have two departments and their names aren't mapped in the database. If that's not correct then I would suggest a different model and approach.
Controller
The controller needs to prepare a model. In this case, we will get all of the data, then transform it to the model structure we need:
public enum Departments // Enumerating a database key like this can be helpful if the database itself doesn't describe the numbers in a lookup table or something
{
IT = 1,
Admin = 2
};
[HttpGet]
public ActionResult Status()
{
var Inventory = context.Inventory.ToList(); // Get all records
var ViewModel = new List<Models.ProductsInDepartments>();
foreach (int ProductId in Inventory.Select(e => e.ProductId).Distinct().ToList())
{
ViewModel.Add(new Models.ProductsInDepartments()
{
ProductId = ProductId,
ProductName = Inventory.First(e => e.ProductId == ProductId).ProductName,
AdminTotal = Inventory.Count(e => e.ProductId == ProductId && e.DepartmentId == (int)Department.Admin),
ITTotal = Inventory.Count(e => e.ProductId == ProductId && e.DepartmentId == (int)Department.IT),
Status = Inventory.First(e => e.ProductId == ProductId).Status // I'm not sure what you are trying to do with Status, so you might need to change this
});
}
return View(ViewModel);
}
View
Now the view is very straightforward.
#model List<ProductsInventory.Models.ProductsInDepartments>
<table class="table">
<tr>
<th>Product Name</th>
<th>IT Department</th>
<th>Administration</th>
<th>Total Products</th>
</tr>
#foreach (var Item in Model)
{
<tr>
<td>#Model.ProductName</td>
<td>#Model.ITTotal.ToString()</td>
<td>#Model.AdminTotal.ToString()</td>
#if (Model.Status == 0)
{
<td>No Products</td>
}
else
{
<td>#Model.Status</td>
}
</tr>
}
</table>
Again, I'm not sure what you're trying to do with status but for string overrides you can do those in the view like this and it's perfectly fine. The view should handle various aspects of presentation-layer concerns.
In #Html.Action the first parameter should be action name and second parameter should be controller name. You need to change your code to match that.
I am trying to pass data using ViewBag.Unions. I get data at controller, but when i foreach loop in view it says 'object' does not contain a definition for 'CountryName'.I give the full code from controller and view. I can not solve this problem.
Controller
public ActionResult Index()
{
List<Country> listCountry = _db.Countries.ToList();
ViewBag.Countries = listCountry;
ViewBag.Unions = (from unon in _db.Unions
join upz in _db.Upazilas on unon.UpazilaId equals upz.UpazilaId
join dic in _db.Districts on upz.DistrictId equals dic.DistrictId
join div in _db.Divisions on dic.DivisionId equals div.DivisionId
join con in _db.Countries on div.CountryId equals con.CountryId
select new
{
con.CountryName,
div.DivisionName,
dic.DistrictName,
upz.UpazilaName,
unon.UnionName
}).ToList();
return View();
}
View
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<table class="">
<thead>
<tr>
<td>Country</td>
<td>Division</td>
<td>District</td>
<td>Upazila</td>
<td>Union</td>
</tr>
</thead>
<tbody>
#if (ViewBag.Unions != null)
{
foreach (var un in ViewBag.Unions)
{
<tr>
<td>#un.CountryName </td>
<td>#un.DivisionName</td>
<td>#un.DistrictName</td>
<td>#un.UpazilaName</td>
<td>#un.UnionName</td>
</tr>
}
}
</tbody>
</table>
Because ViewBag is a dynamic type dictionary. So each of your item in that collection are dynamic type. The compiler skips the type checking when you try to access a property of an dynamic type object, but it might fail in run time (This is exactly one of the reason i am avoiding ViewBag/ViewData as much as possible).
What you should be doing is, create a view model to represent this data and project to that in your LINQ expression
public class MyViewModel
{
public string CountryName { set;get;}
public string DivisionName { set;get;}
public string DistrictName { set;get;}
}
Now since you have a strongly typed class, you do not really need ViewBag to pass the data. You can directly pass the list of MyViewModel objects to the view.
var items = (from unon in _db.Unions
join upz in _db.Upazilas on unon.UpazilaId equals upz.UpazilaId
join dic in _db.Districts on upz.DistrictId equals dic.DistrictId
join div in _db.Divisions on dic.DivisionId equals div.DivisionId
join con in _db.Countries on div.CountryId equals con.CountryId
select new MyViewModel
{
CountryName = con.CountryName,
DivisionName = div.DivisionName,
DistrictName = dic.DistrictName
}).ToList();
return View(items);
Now make sure your view is strongly typed to this collection type
#model List<MyViewModel>
<table class="table>
#foreach(var item in Model)
{
<tr>
<td>#item.CountryName</td>
<td>#item.DivisionName</td>
<td>#item.DistrictnName</td>
</tr>
}
</table>
If you still want to use ViewBag to pass the data (but why ???), you can do that. Instead of passing the list of items to the view method, you can set it to view bag and access it in your razor view. Make sure to cast it to a list MyViewModel before you start looping the collection.
ViewBag.Items = items;
return View();
and in the view
<table class="table>
#foreach(var item in ViewBag.Items as List<MyViewModel>)
{
<tr>
<td>#item.CountryName</td>
<td>#item.DivisionName</td>
<td>#item.DistrictnName</td>
</tr>
}
</table>
I'm trying to get all the info (column, rows) from a table in SQL and send it as a model to the view.
I also want it to be Distinct.
My controller:
MVCEntities me = new MVCEntities();
List<CarsCategory> arr = me.CarsCategories.ToList();
return View(arr);
My view model:
#model IEnumerable<CarsCategory>
In the view I'm trying to loop through a certain column like this:
<select id="SelectManufacturer">
#foreach (var i in Model)
{
<option value="#i.Manufacturer">#i.Manufacturer</option>
}
</select>
How do I make it Distinct? When I try to add Distinct it gives me system.linq.enumerable+<DistinctIterator> ..
Although it's not a good approach to process data inside the view, your solution might look like this:
<select id="SelectManufacturer">
#{
var manufacturers = Model.Select(x => x.Manufacturer).Distinct().ToList();
foreach (var i in manufacturers)
{
<option value="#i">#i</option>
}
}
</select>
The controller should be responsible to supply the View with the data, the view should not be polutted with a bunch of logic to try to aggregate this data unless you want unmaintainable code. The best approach is to extend your view model to have multiple properties.
Models
public class CategoryModel{
public List<CarsCategory> CarCategories {get;set;}
public List<Manufacturer> Manufacturers {get;set;}
}
public class Manufacturer{
public int Id {get;set;}
public string Name {get;set;}
}
Controller code
// you need to ensure that if you are using EF the context is disposed after you are done using it!
using(MVCEntities me = new MVCEntities()) {
var model = new CategoryModel();
model.CarCategories = me.CarsCategories.ToList();
// you need to supply the correct Id and Name locations in your model as you did not share this
model.Manufacturers = model.CarCategories.Select(x => new Manufacturer(){Id = x.prop.id, Name = x.prop.name}).Distinct();
return View(model);
}
Razor View
#model CategoryModel
<select id="SelectManufacturer">
#foreach (var i in Model.Manufacturers)
{
<option value="#i.Id">#i.Name</option>
}
</select>
I know there are a lot of topics about this issue, but none answers this specific problem of mine. I have a MVC project in which I want to implement two models on one View (separate tables).
I used the validated suggestion from the link to execute just that:
How do I view the parent view model MVC3 C#?
Here's my code:
-First model
[Table("Issue_Tracker")]
public class Case
{...
}
-Second model:
[Table("Jobs_Ref_Tbl")]
public class Job
{...
}
-Composite model:
public class IndexPageModel
{
public IEnumerable<Case> Cases { get; set; }
public IEnumerable<Job> Jobs { get; set; }
}
-Creating my CaseDBContext:
public class CaseDBContext : DbContext
{
public DbSet<Case> Cases { get; set; }
public DbSet<Job> Jobs { get; set; }
public DbSet<Server> Servers { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Department> Departments { get; set; }
}
In the controller I have sorted and filtered version of Cases model, using LINQ queries. After all the sorting and filtering, I used to pass an IPagedList of cases model to the view.
Now I have:
var cases = db.Cases.AsQueryable();
[sorts and filters]
var model = new IndexPageModel
{
Jobs = db.Jobs.ToPagedList(page ?? 1, 5),
Cases = cases.ToPagedList(page ?? 1, 10)
};
return View(model);
And finally I use them in the View:
<!DOCTYPE html>
#using PagedList;
#using PagedList.Mvc;
#model IPagedList<ITS.Models.IndexPageModel>
...
<table id="tableBe">
<tr>
<th style="border-left:none !important">
Action Buttons
</th>
<th>
<div style="width: 250px">
#Html.DisplayNameFor(model => model.Cases.First().Issue)
And here I run into the problem
'IPagedList' does not contain a definition for 'Cases' and no extension method 'Cases' accepting a first argument of type 'IPagedList' could be found (are you missing a using directive or an assembly reference?)
It's as if I did not include them in the model at all. Any advice on how to solve this problem would be appreciated.
EDIT: ADDITIONAL INFO
The code is very long, there are many filters in order to support combined search function. The basics consist of the 5th code snippet in my post:
I declare my IQueryable then manipulate it and after that I insert it into the model as the cases variable. db.Jobs I leave intact and thats why I insert it directly into the model variable.
It used to be:
var cases = db.Cases.AsQueryable();
[sorts and filters]
return View(cases.ToPagedList(page ?? 1, 10));
I want it to be:
var cases = db.Cases.AsQueryable();
[sorts and filters]
var model = new IndexPageModel
{
Jobs = db.Jobs.ToList()
Cases = cases
};
return View(model.ToPagedList(page ?? 1, 10));
But it returns
'IndexPageModel' does not contain a definiton for 'ToPagedList'
Your controller is returning a IndexPageModel, but your view wants a IPagedList<ITS.Models.IndexPageModel>. Change your model to accept the correct model:
<!DOCTYPE html>
#using PagedList;
#using PagedList.Mvc;
#model ITS.Models.IndexPageModel // The correct model.
Based on model => model.Cases.First().Issue it seems that you actually want to use the model returned from the controller (IndexPageModel).
I assume ToPagedList() returns an IPagedList<T>. Which means that you actually just want to use IPagedList in the IndexPageModel, but not in the actual view. So based on that you should be able to simply change the model in the view and it will work.