I am trying to bind the HTML table using the View Model of type Object on the Razor Page. Code is as below :
index.cshtml.cs
[BindProperty]
public List<object> TableData { get; set; }
public class Cainfo
{
public string Ca_id { get; set; }
public object Nca { get; set; }
}
public async Task<IActionResult> OnGetAsync()
{
List<object> tablerows = GetTableRows(tabledata);
TableData = tablerows;
}
public List<object> GetTableRows(GetTableRowsResponse getTableRowsResponse)
{
List<object> tableRows = new List<object>();
var tables = getTableRowsResponse.rows;
foreach (var table in tables)
{
var tab = JsonConvert.SerializeObject(table);
var row = JsonConvert.DeserializeObject<Cainfo>(tab);
tableRows.Add(row);
}
return tableRows;
}
index.cshtml
<table class="resultTable">
<thead class="grid-header">
<tr>
#foreach (var property in #Model.TableData.GetType().GetGenericArguments()[0].GetProperties())
{
<th class="col-lg-1">#property.Name</th>
}
</tr>
</thead>
<tbody class="grid-body">
#if (#Model.TableData != null)
{
#if ((#Model.TableData.Count != 0))
{
#foreach (var row in Model.TableData)
{
<tr>
#foreach (var property in #row.GetType().GetProperties())
{
<td class="col-lg-1">#property.GetValue(#row)</td>
}
</tr>
}
}
}
</tbody>
</table>
var tables = getTableRowsResponse.rows; return the JSON data. Problem is that table <th> is not getting bind. #Model.TableData.GetType().GetGenericArguments()[0].GetProperties() is getting empty. <td> is getting bind as expected. Maybe I am doing a mistake somewhere, I am new to the asp .net core. Now, I am using the Model Cainfo but in future, I need to set different models according to data and bind the same table. That's why I am giving View Model type as Object. Please help.
I would not use reflection like this when you can design common models for the view. That's the art of designing which makes things easier. However here assume that you want to stick with that solution anyway, I'll show where it's wrong and how to fix it and how it's limited.
First this Model.TableData.GetType().GetGenericArguments()[0] will return Type of object. And that Type of course contains no properties. That's why you get no <th> rendered. The generic argument type exactly reflects what's declared for Model.TableData which is a List<object>.
Now to fix it, assume that all the items in the List<object> are of the same type, you can get the first item's Type, like this:
#foreach (var property in #Model.TableData?.FirstOrDefault()
?.GetType()?.GetProperties()
?? Enumerable.Empty<PropertyInfo>())
{
<th class="col-lg-1">#property.Name</th>
}
That has a limit in case the Model.TableData contains no item. No <th> will be rendered. If that's acceptable (instead of rendering an empty table with headers, you will render nothing or just some message) then just go that way. Otherwise, you need to provide a Type for the element/row's Typevia your model, such as via a property like Model.RowType. Then you can use that instead of this:
Model.TableData?.FirstOrDefault()?.GetType()
The remaining code is just the same.
Related
I changed the project design and created database view :
ALTER VIEW [dbo].[samplesList]
AS
SELECT DISTINCT
results.machine_id,
sample_id,
program_id,
Machines.Machine_id AS 'ID',
custid,
sys_users.user_full_name AS 'HospitalName',
Programs.name AS 'ProgramName',
machines.Machine_name AS 'MachineName',
samples.name AS 'SampleName'
FROM
results
INNER JOIN
programs ON RESULTS.program_id = Programs.id
INNER JOIN
Machines ON RESULTS.machine_id = Machines.Machine_id
INNER JOIN
sys_users ON RESULTS.custid = sys_users.user_id
INNER JOIN
samples ON RESULTS.sample_id = samples.id
This is the result in the database :
See the screenshot - it shows the correct data sample no 1, sample no 2, sample no 3 and their machines are correct.
But in the controller when I link the view with the controller its not show same result from the database this is the controller code :
public ActionResult Indexs()
{
int UserId = Convert.ToInt32(Session["UserID"]);
var samples = _context.samplesLists
.Where(x=> x.custid == UserId).ToList();
return View(samples);
}
This is the model :
namespace warehouse.Models
{
using System;
using System.Collections.Generic;
public partial class samplesList
{
public Nullable<int> machine_id { get; set; }
public Nullable<int> sample_id { get; set; }
public Nullable<int> program_id { get; set; }
public int ID { get; set; }
public Nullable<int> custid { get; set; }
public string HospitalName { get; set; }
public string ProgramName { get; set; }
public string MachineName { get; set; }
public string SampleName { get; set; }
}
}
And finally the surprise this is the output for same view in the site :
All data appears as "sample no 1":
This is the view markup:
#model IEnumerable<warehouse.Models.samplesList>
#{
ViewBag.Title = "Indexs";
Layout = "~/Views/Shared/_LayoutDashboard.cshtml";
}
<img style="margin-left:250px;" src="~/images/weblogo.png" />
<p style="margin-left:40px;">
#*<h3 style="margin-left:100px; font-family:Andalus;text-underline-position:below">
#Html.Label("Hospital Name :")
#Html.DisplayFor(model => Model.FirstOrDefault().)
</h3>*#
<table class="table table-bordered">
<tr style="background-color:hotpink">
<th>
#Html.DisplayNameFor(model => model.ProgramName)
</th>
<th>
#Html.DisplayNameFor(model => model.SampleName)
</th>
<th>
#Html.DisplayNameFor(model => model.MachineName)
</th>
#*<th></th>
<th></th>
<th></th>*#
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.ProgramName)
</td>
<td>
#Html.DisplayFor(modelItem => item.SampleName)
</td>
<td>
#Html.DisplayFor(modelItem => item.MachineName)
</td>
Please I need your help what happening why all solutions not working and what I need to change ?
EF has its share of troubles with views - because views typically don't have a defined primary key, which is crucial for EF to detect which data has already been loaded (and which hasn't).
What happens here is: since there's no primary key for your view, EF will just use all non-nullable columns from the view as a "substitute" PK.
And when EF reads the data, it will go:
read the row
check the primary key (or the "substitute" PK in this case)
if it has already read a row with that PK - it just duplicates that row that it already has - it will disregard any non-PK columns actually read from the view!
So in your case, once it's read a first row with the "substitute" PK, any further rows with those same values will just get the already read values - no matter what's stored in the database!
SOLUTION: EF needs a proper primary key to uniquely identify each individual row of data.
One solution is to add every single PK of all the underlying tables, and make sure those are marked as "non-nullable" in your model class. Another solution might be to add a custom ROW_NUMBER() OVER (....) column as an "artificial" dummy PK which gives each row a unique value - so that no two rows being read from the view are considered identical.
I solved it in different way as marc_s said there is different activity in database views and primary keys not deal like the tables primary key
What I did :
I created new TABLE (MASTER_RESULTS) and inserted the data I need without duplicate and created the controller and view based on this master table .
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 want to do something similar to what is explained in this question: I want to edit a list of data.
The difference is that the base is not a list.
(I'm using VS 2013, so it is not old stuff.)
My view model:
public class SampleViewModel
{
// ... other properties for editing ...
// The list property
public List<SampleListItemViewModel> ItemList { get; set; }
}
public class SampleListItemViewModel
{
// For display only. It has an ID field to identify the row.
public MyEntity Item { get; set; }
// I want to modify this!
public bool IsChecked { get; set; }
}
My attempted View:
#model My.Namespace.SampleViewModel
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<!-- ... normal editing, MVC generated ... -->
<!-- BEGIN LIST EDIT -->
<table>
<tr>
<th>
Is Applicable
</th>
<!-- ... -->
</tr>
#foreach (var doc in Model.ItemList)
{
#Html.HiddenFor(modelItem => doc.Document.CRMDocumentId)
<tr>
<td>
#Html.EditorFor(modelItem => doc.IsChecked)
<!-- #Html.ValidationMessageFor(modelItem => doc.IsChecked) -->
</td>
<!-- ... other non-editable display fields, e.g. name ... -->
</tr>
}
<!-- END LIST EDIT -->
<!-- ... -->
}
When I create the view it shows everything as I want it, but when I click "Create" the item list is null.
EDIT - More Info
Controller:
public ActionResult Create(int? id)
{
var item = // ...populate...
// I confirmed that ItemList has values.
return View(item);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SampleViewModel item)
{
// This is null
var list = item.ItemList;
// ignoring all else for now
return View(item);
}
To bind complex objects, we need to provide an index for each item, rather than relying on the order of items. This ensures we can unambiguously match up the submitted properties with the correct object.
Replace the foreach loop with for loop:
#for (int i=0; i<Model.ItemList.Count; i++)
{
#Html.HiddenFor(modelItem => modelItem.ItemList[i].Document.CRMDocumentId)
<tr>
<td>
#Html.EditorFor(modelItem => modelItem.ItemList[i].IsChecked)
#Html.ValidationMessageFor(modelItem => modelItem.ItemList[i].IsChecked)
</td>
</tr>
}
NOTE:
Note that the index must be an unbroken sequence of integers starting at 0 and increasing by 1 for each element
Also for every Property in the list which you don't want user to edit like for example UserId etc add an #Html.HiddenFor(..) for that property otherwise it will posted null to the server and if hidden field created for them they will not be NULL.
For further details you can see HERE
Also refer Model Binding with List
The base functionality I wish to achive is that the contents of a table are updated when a dropdownlist item is selected. This will update when the user makes a new selection and retrieve new information from the database and repopulate the table.
It's also worth noting that the DropDownListFor that I want the .change() to work with is not contained within the AjaxForm but appears elsewhere on the page (admittedly in another form)
To achieve this I looked at this question: Rendering partial view dynamically in ASP.Net MVC3 Razor using Ajax call to Action which does a good job of going part the way of what I want to do.
So far, I have a controller method which handles populating a customized viewmodel for the partial view:
[HttpPost]
public ActionResult CompanyBillingBandDetails(int id = 0)
{
var viewModel = new BillingGroupDetailsViewModel();
var billingGroupBillingBands =
_model.GetAllRecordsWhere<BillingGroupBillingBand>(x => x.BillingGroupId == id).ToList();
foreach (var band in billingGroupBillingBands)
{
viewModel.BillingBands.Add(band.BillingBand);
}
return PartialView("BillingGroupDetailsPartial", viewModel);
}
The ViewModel I wish to populate each call:
public class BillingGroupDetailsViewModel
{
public List<BillingBand> BillingBands { get; set; }
}
The strongly typed model I'm using as a model for the partial view
public class BillingBandsObject
{
public int BillingBandId { get; set; }
public int RangeFrom { get; set; }
public int RangeTo { get; set; }
public Decimal Charge { get; set; }
public int BillingTypeId { get; set; }
public bool Delete { get; set; }
}
The partial view it populates and returns:
#model xxx.xxx.DTO.Objects.BillingBandsObject
<tr>
<td>
#Html.DisplayTextFor(x => x.RangeFrom)
</td>
</tr>
<tr>
<td>
#Html.DisplayTextFor(x => x.RangeTo)
</td>
</tr>
<tr>
<td>
#Html.DisplayTextFor(x => x.Charge)
</td>
</tr>
The Razor code for this section of the page:
<table>
<thead>
<tr>
<th>
Range From
</th>
<th>
Range To
</th>
<th>
Charge
</th>
</tr>
</thead>
<tbody>
#using (Ajax.BeginForm("CompanyBillingBandDetails", new AjaxOptions() { UpdateTargetId = "details", id = "ajaxform" }))
{
<div id="details">
#Html.Action("CompanyBillingBandDetails", new { id = 1 })
</div>
}
</tbody>
</table>
and finally the function I lifted almost straight from Darin's answer:
$(function () {
$('#billinggrouplist').change(function () {
// This event will be triggered when the dropdown list selection changes
// We start by fetching the form element. Note that if you have
// multiple forms on the page it would be better to provide it
// an unique id in the Ajax.BeginForm helper and then use id selector:
var form = $('#ajaxform');
// finally we send the AJAX request:
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
success: function (result) {
// The AJAX request succeeded and the result variable
// will contain the partial HTML returned by the action
// we inject it into the div:
$('#details').html(result);
}
});
});
});
At the moment I have fought through a number of errors, currently I am faced with :
"Error executing child request for handler 'System.Web.Mvc.HttpHandlerUtil+ServerExecuteHttpHandlerAsyncWrapper'."
However, i feel my understanding of the problem as a whole may be lacking.
Any help appreciated!
This error means that there was an exception while rendering your child view. Probably something related to your data, ie. NulLReferenceException.
Just attach your debugger and set to to break when an exception is thrown.