I have this HTML page in an ASP .NET project, and I need to generate a pagination for a table of elements. Problem is, in the class architecture that's already in place, the only thing I can have for my model is an IEnumerable of a view model. The ONLY thing I would need is to fetch an integer value from either my controller or the view it returns. From that integer, that would represent the number of buttons needed to generate the pagination, I would create it, see HTML.
My controller generates the list of items and returns in the model it by doing a SQL Request that selects a certain amount of item from a certain offset, depending on my URL's parameters.
Here's the HTML, and what the code-behind in the controller would look like:
#model IEnumerable<ItemIndexViewModel>
<h2>#UiText.PageTitles.ITEM_LIST</h2>
<hr />
<div class="col-md-12">
<div class="col-md-9">
<table class="table" id="client-index">
<thead>
<tr>
<th class="green-table-head-1">
#Html.DisplayNameFor(model => model.Name)
</th>
<th class="green-table-head-1">
#Html.DisplayNameFor(model => model.Id)
</th>
</tr>
</thead>
<tbody>
#foreach (ItemViewModel item in Model)
{
#*Here, I have my table of items being generated*#
}
</tbody>
</table>
<div id="pagination">
<ul>
#for (int i = 0; i < [I need my int right here]; i++)
{
#*I will generate buttons here*#
}
</ul>
</div>
</div>
</div>
public virtual ActionResult Index()
{
int ownerId = _httpContext.GetUserOwnerId();
int amountPerPage = 0;
int pageIndex = 0;
Int32.TryParse(Request.QueryString["amountPerPage"], out amountPerPage);
Int32.TryParse(Request.QueryString["pageIndex"], out pageIndex);
if (amountPerPage <= 0)
{
amountPerPage = 10;
}
if (pageIndex <= 0)
{
pageIndex = 1;
}
List<Item> items = _itemRepository.GetByPage(pageIndex, amountPerPage).ToList();
// Make view models from the list of items
List<ItemIndexViewModel> itemIndexViewModels = Mapper.Map<List<ItemIndexViewModel>>(items);
// Create the buttons for the HTML
int totalAmount = _itemRepository.Count();
int totalPages = (Int32)Math.Ceiling(Decimal.Divide(totalAmount, amountPerPage));
// Set update the navigation trace
SetTraceRoot(MVC.Item.Index(), MVC.Item.ActionNames.Index);
return View(itemIndexViewModels.OrderBy(x => x.Name));
}
What would be a good way of generating a pagination? I'm looking for flexibility because this procedure will be implemented for more than one page and for more than one class of items. I've already tried a couple of things to no avail, like using a class to contain my list of view models and an integer for the number of pages needed to store them all.
You can use ViewBag or ViewData instance in controller action to provide paging value before returning view:
ViewBag.TotalPages = totalPages;
And pass its value to the for loop to generate pagination in view side:
#for (int i = 0; i < ViewBag.TotalPages; i++)
{
#* generate paging buttons *#
}
Usually ViewBag properties don't require type cast for simple types (numeric & string values), but ensure that the value is assigned to avoid return null.
Related
I have a Razor view that lists holiday park accomodations in a table. The user - a park manager - has decided that these should be removed, and now presses the Remove button. The Ids of all of the listed accomodations then must be passed to the controller, but somehow they don't arrive and I cannot lay my finger on why not. Here is the code of the view:
#model SunnySideWebManagement.ViewModels.HomeAccomodationsViewModel
#{
ViewBag.PageTitle = "Remove these accomodations?";
int[] removeIds = new int[Model.Accomodations.Count()];
}
<form asp-controller="home" asp-action="removeaccomodation" asp-route-removeIds="#removeIds" method="post" class="mt-10">
<div class="form-group row">
<table id="accomodationsTable" class="row-border">
<thead>
<tr><th>Id</th><th>Number</th><th>Remarks</th><th>Currently booked</th></tr>
</thead>
<tbody>
#{
int index = 0;
foreach (var ac in Model.Accomodations)
{
removeIds[index] = ac.Id;
index++;
<tr><td>#ac.Id</td><td>Number</td><td>#ac.Remarks</td><td>#ac.CurrentlyBooked</td></tr>
}
}
</tbody>
</table>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Remove</button>
<a asp-controller="home" asp-action="accomodations" class="btn btn-primary">Cancel</a>
</div>
</div>
</form>
As you can see, I declare an int array removeIds and populate it pretty straightforward as the table gets filled. removeIds receives the correct values.
The Remove button click correctly calls the following code in HomeController.cs, as defined in the <form ...> element:
[HttpPost]
public IActionResult RemoveAccomodation(int[] removeIds)
{
for (int i = 0; i < removeIds.Length; i++)
{
_accomodationRepository.Delete(removeIds[i]);
}
return RedirectToAction("accomodations");
}
A breakpoint here reveals that removeIds is an empty array - what happened to its values? What do I overlook, or what do I do wrong?
Using submit you can only post a view model and nothig else and I don't see any sense in creating removeIds inside of the view, since you are not selecting anything. Since you are using form and submit button , your whole model will be submitted in any case, doesn't matter if you use asp-route or not. So remove asp-route-removeIds="#removeIds" and fix the action.
[HttpPost]
public IActionResult RemoveAccomodation(HomeAccomodationsViewModel model)
{
int[] removeIds = Model.Accomodations.Select(i=> i.Id).ToArray();
for (int i = 0; i < removeIds.Length; i++)
{
_accomodationRepository.Delete(removeIds[i]);
}
return RedirectToAction("accomodations");
}
PS.
if you want submit only Ids, you have to use an ancor tag, instead of a form, or create a special view model with array of ids. If you want to use ids in ancor, you have to fill all Ids before creating the ancor, or use javascript. But your code as it is now doesn't make much sense.
PS2
if you still want to return all accomodation items back you have to use for loop instead of foreach loop
#for (var i=0; i < Model.Accomodations.Count; i++)
{
<tr><td>#Model.Accomodations[i].Id</td><td>Number</td>
<td>#Model.Accomodations[i].Remarks</td>
<td>#Model.Accomodations[i].CurrentlyBooked</td></tr>
}
but instead of this you can use just I to get all accomodations from db.
Not even sure if I asked the question the right way. Been looking at this for about an hour and its too simple to take to long. Trouble is I am too simple to know the answer or even how to correctly phrase a search to find the answer.
I have a history of jobs completed for a site set up.
Controller:
public async Task<IActionResult> JobSiteHistory(int id, int? page)
{
var jobs = from j in _context.Job
.Include(j => j.Site)
.Include(j=>j.WaterBody)
.Where(j=>j.Site.SiteID==id)
.OrderByDescending(j=>j.BookingDate)
select j;
int pageSize = 9;
return View(await PaginatedList<Job>.CreateAsync(jobs.AsNoTracking(), page ?? 1, pageSize));
}
This is returning the correct records all good.
I then have a view set up:
<h2> Site Jobs History</h2>
<p>
<a asp-action="Create">Add New Job</a>
</p>
<table class="table">
<thead>
<tr>
<th>Booking Date</th>
<th>Job Number</th>
<th>Waterbody</th>
<th>Job Description</th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model) {
<tr>
<td>#Html.DisplayFor(modelItem => item.BookingDate)</td>
<td>#Html.DisplayFor(modelItem => item.JobNumber)</td>
<td>#Html.DisplayFor(modelItem => item.WaterBody.WBName)</td>
<td>#item.JobDescription.Substring(0, Math.Min(item.JobDescription.Length, 30))</td>
<td>
<a asp-action="Edit" asp-route-id="#item.JobID">Edit</a> |
<a asp-action="Details" asp-route-id="#item.JobID">Details</a> |
<a asp-action="Delete" asp-route-id="#item.JobID">Delete</a> |
</td>
</tr>
}
</tbody>
</table>
This is working wellish so far.
All I want to do is add something like:
#Html.DisplayFor(ModelItem=>item.Site.SiteName)
To the <h2> element. I know this wont work as typed, thanks for thinking that.
I just cant see a way to add it. I considered ViewData, but may be using it wrong as I cant get it to populate with SiteName.
Is there a way to do this or am I thinking all ass about as usual?
The easiest change would be to use this:
<h2>#Html.DisplayFor(m => m[0].Site.SiteName);</h2>
Other options:
Is there any reason why you can't use the ViewBag?
In controller:
ViewBag.SiteName = Site.name
In view:
<h2>#ViewBag.SiteName</h2>
If you must use your model to pass the whole site object then change your view model that you pass to the view.
You are currently returning a list of jobs with the site object for each job, but it looks like you only need it once.
I would change your view model to be something like:
public class SiteJobsHistoryModel
{
public Site Site { get; set;}
public PaginatedList<Job> Jobs { get; set; }
}
Then you don't have to include the site on your query, and just retrieve it once from the database:
var site = _context.Site.Single(j => j.Site.SiteID==id);
var jobs = from j in _context.Job
//.Include(j => j.Site) -- this can be removed
.Include(j=>j.WaterBody)
.Where(j=>j.Site.SiteID==id)
.OrderByDescending(j=>j.BookingDate)
select j;
return View(new SiteJobsHistoryModel
{
Site = site,
Jobs = await PaginatedList<Job>.CreateAsync(jobs.AsNoTracking(), page ?? 1, pageSize)
});
Then for the title in <h2> tag you can use:
#Html.DisplayFor(ModelItem=>model.Site.SiteName)
And your foreach loop becomes:
#foreach (var item in Model.Jobs)
I pass a List to a partial view and it works fine, it shows all the data but when I save the Model, the List returns null, what am I missing?
Dont pay attention to the objects, I wrote fake ones for the example.
This the cshtml:
#model ViewModels.StudentVM
#using (Html.BeginForm("SaveStudent", "StudentsView", FormMethod.Post}))
{
#Html.AntiForgeryToken();
<div class="row">
<span>Student name:</span>
#Html.TextBoxFor(s => s.Name)
</div>
<div>
#Html.Partial("StudentsList", Model.Students)
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn">
</div>
}
When loading the view I get all the students to the View Model:
vm.Students = await _studentController.GetAllStudents(); // returned 20 Students.
The partial view:
#model IEnumerable<Entities.Students>
<table class="table-bordered">
#foreach (var item in Model)
{
<tr>
<td>
#Html.CheckBoxFor(modelItem => item.IsSelected)
</td>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}
</table>
I would like to get all selected students, so lets say I will select 3 students.
And then click on the save button. Result: the Model.Students is null although it I selected 3 students. How can I get those students?
Your current code of partial view has only the Call to DisplayNameFor helper method which will only render the display name as a label. If you want to submit the data of each item, you need to generate input form fields with matching names with your view model property structure.
Assuming your StudentVM has a Students collection of type IEnumerable<Students>
If your HttpPost action method's parameter is of type StudentVm, you need to make sure that your partial view is generating the input form fields with name like 'Students[0].Name, 'Students[1].Name etc. You can use the Html.TextBox helper method and specify this custom names
#model IEnumerable<Students>
<table class="table-bordered">
#{
var counter = 0;
}
#foreach (var student in Model)
{
<tr>
<th>Name</th>
<td>#Html.TextBox("Students[" + counter + "].Name", student.Name)</td>
</tr>
counter++;
}
</table>
If you are simply displaying the names of existing students, you do not need the text fields, you can simply display those inside the loop. In that case, when form submits,why do you worry about the Students collection being null ? You are trying to save a new Student which will be in .Name property. So if you need the existing students again (but why ? ), you can call the GetAllStudents method.
maybe you used 2 Different model in view ......
you can use 2 model in 1 viewmodel
It is not necessary partial view
ViewModels.StudentVM != IEnumerable<Entities.Students>
you can all data (many model) passed in 1 view model
:
var vm = new TestViewModel();
vm.one = db.one.tolist();
vm.two = db.two.tolist();
It's my first asp.net mvc application at all. I'm using mvc 3 and razor for my views. The specific view with which I have problem is a strongly-typed view (at least I think it is) but it accepts type of this kind :
#model List<List<DataAccess.MCS_DocumentFields>[]>
I made several partial view because the main view was very customized and I need a lot of different logic for different parts of my data. My partial view are also strongly typed (again I think this is correct to say) of type :
#model List<DataAccess.MCS_DocumentFields>[]
Everything that I need to build my view is in :
#using (Html.BeginForm("ActionMethodName", "Forms", FormMethod.Post))
{
<div id="drawForm">
<table border="1">
Inside Hhtml.BeginForm I build table with all the data that comes from the controller. Now on submit :
<button type="submit">Submit</button>
I'm looking for the best way to get my data, and update in the database what is changed. In my controller I have this method :
[HttpPost]
public ActionResult ActionMethodName(FormCollection collection)
{
var test = collection;
List<MCS_Documents> model = DocumentsService.All().ToList();
return View("Index", model);
}
This way I can find my data, but it's so complicated, I'm not even sure how I'll get to it. I tried something else, using the same method but accepting as argument the type which the view accepts :
[HttpPost]
public ActionResult ActionMethodName(List<List<DataAccess.MCS_DocumentFields>[]> collection)
{
But when I do that collections has null value. And from the little I know when I use strongly-typed view I can get some additional advantages like form.validation and data binding ready for update and so on...
So what is the best way, in my specific scenario, to deal with the data submitted from the form?
P.S
This is how I render my main view :
<table border="1">
<colgroup>
<col span="1" style="width: 10%;" />
<col span="1" style="width: 40%;" />
<col span="1" style="width: 25%;" />
<col span="1" style="width: 25%;" />
</colgroup>
<tbody>
#for (int i = 0; i < Model.Count(); i++)
{
if (Model[i][0][0].ContentTypeId == 1)
{
#Html.Partial("_PartialHeader", Model[i])
}
else if (Model[i][0][0].ContentTypeId == 2)
{
#Html.Partial("_PartialDrawing", Model[i])
}
else if (Model[i][0][0].ContentTypeId == 3)
{
#Html.Partial("_PartialBody", Model[i])
}
else if (Model[i][0][0].ContentTypeId == 4)
{
#Html.Partial("_PartialFooter", Model[i])
}
}
</tbody>
</table>
<button type="submit">Save</button>
and this is one of my partials, just to showcase how I use them :
#model List<DataAccess.MCS_DocumentFields>[]
<tr>
#if (!string.IsNullOrEmpty(Model[0][0].FieldValue))
{
<td colspan="2">
#Html.DisplayFor(x => x[0][0].FieldValue)
</td>
}
else
{
<td colspan="2">
Sign in here
</td>
}
#if (!string.IsNullOrEmpty(Model[1][0].FieldValue))
{
<td colspan="2">
#Html.DisplayFor(x => x[1][0].FieldValue)
</td>
}
else
{
<td colspan="2">
Sign in here
</td>
}
</tr>
That last action method is correct (which takes your strong type), but your views will need to be structured differently. You need to make sure that every collection (even the sub collections) are correctly indexed, otherwise model binding will fail.
Your views should be something like the following (note the use of for loops):
Main view:
#model List<List<DataAccess.MCS_DocumentFields>[]>
#for (int i = 0; i < Model.Count; i++)
{
#Html.RenderPartial("PartialName", Model[i])
}
Then in your Partial:
#model List<DataAccess.MCS_DocumentFields>[]
#for (int i = 0; i < Model.Count; i++)
{
//Not sure if you need anything at this level
for (int j = 0; j < Model[i].Count(); j++)
{
//Add your EditorFor's, HiddenFor's etc for the child type
}
}
I have this view based on a list of a model where I create strongly-typed checkboxes for each items of the model based on a boolean.
Here's my view:
#using MyApp.Models
#model IList<MyApp.Models.ObjInfo>
#{
ViewBag.Title = "Obj Inventory";
}
<h2>Search Inventory</h2>
<p>
#using (Html.BeginForm())
{
(Many search filters which are non-relevant)
<p>
Send Items: #Html.ActionLink("Click Here", "SendItems")
</p>
}
<table>
<tr>
<th>
Obj Name
</th>
<th>
Number In Stock
</th>
(...)
<th>
Select Item
</th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.OtherObj.m_Name)
</td>
(...)
<td>
#Html.CheckBoxFor(modelItem => item.m_IsSelected)
</td>
</tr>
}
</table>
The whole process works fine and I can actually generate a view with checkboxes for each item of my list of model.
Now my question is that I want to create a list which would regroup only the items in the list which are checked and send them to the controller. How could I do that? Can anyone help me or suggest me a way to work?
Thank you!
* EDIT *
Here is the HttpPost Method used to get the List of items as mentioned below:
//
// GET: /Inventory/SendItems
[HttpPost]
public ActionResult SendItems(IList<ObjInfo> listToSend)
{
m_ListObjToSend = new List<ObjInfo>();
foreach (var item in listToSend.Where(item => item.m_IsSelected))
{
m_ListObjToSend .Add(item);
}
return View(m_ListObjToSend );
}
However I have encountered many problems:
This method does NOT work if I put the [HttpPost] attribute (it will show as "Not Found");
The list I am supposed to receive is null;
Each hiddenfield linked with the checkbox has default value as false even if the checked value shows true;
I am using an actionlink because I do not want to use a button, there is already one that is doing another job.
I am open for any comments / help available, thank you!
If you use the CheckBoxFor helper to generate checkboxes you will notice that it generates an additional hidden field along with each checkbox. This means that all values will be sent to the controller and you will have to filter in your controller those that are checked.
Also I would recommend you using indexes to ensure proper model binding. You just need to use an IList<ObjInfo> or ObjInfo[] which is trivially easy achievable by calling .ToList() or .ToArray() extension methods on your view model before passing it to the view:
#using MyApp.Models
#model IList<ObjInfo>
...
#for (var i = 0; i < Model.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(x => x[i].OtherObj.m_Name)
</td>
(...)
<td>
#Html.CheckBoxFor(x => x[i].m_IsSelected)
</td>
</tr>
}
...
And now your controller action could directly take the list of items:
[HttpPost]
public ActionResult SomeAction(IEnumerable<ObjInfo> model)
{
...
}
and if you wanted to find the selected values, you could simply get them through LINQ:
[HttpPost]
public ActionResult SomeAction(IEnumerable<ObjInfo> model)
{
var selectedItems = model.Where(x => x.m_IsSelected);
...
}
Remark: m_Name and m_IsSelected is a disastrously bad naming convention for a properties in C#.
UPDATE:
Another issue you have with your code is that your Html.BeginForm doesn't contain any input field. It has only a single ActionLink which obviously only does a GET request. If you want to submit the values you should wrap your entire table with the form and use a submit button and not some action links:
#using MyApp.Models
#model IList<ObjInfo>
#{
ViewBag.Title = "Obj Inventory";
}
<h2>Search Inventory</h2>
<p>
#using (Html.BeginForm("SendItems", null, FormMethod.Post))
{
(Many search filters which are non-relevant)
<table>
<tr>
<th>Obj Name</th>
<th>Number In Stock</th>
(...)
<th>Select Item</th>
</tr>
#for (var i = 0; i < Model.Count; i++)
{
<tr>
<td>
<!--
This will not be sent to your controller because it's only a label.
You will need a corresponding hidden field if you want to get that value back
-->
#Html.DisplayFor(x => x[i].OtherObj.m_Name)
</td>
(...)
<td>
#Html.CheckBoxFor(x => x[i].m_IsSelected)
</td>
</tr>
}
</table>
<p>
Send Items: <button type="submit">Click Here</button>
</p>
}
</p>
So really, 2 things you should learn:
The naming convention that the default model binder expects when binding to a list
How to use a javascript debugging tool (such as FireBug and/or Chrome Developper Toolbar) which will allow you to inspect all the values that are sent to your server and immediately recognized whether you respected the convention you learned in 1.