ASP.NET paging is reset with multiple forms - c#

My web interface has a control panel built in Razor Pages, showing varius devices admins can configure, displayed in a table with varius buttons on the side for quick actions and info (like a toggle to show enabled and connection status).
The way i built it initially it had no paging, and a simple loop with a forms column for quick actions, based on the relative row
#{
ViewBag.BaseLayout = "/Views/Shared/_Layout_Areas.cshtml";
Layout = "/Views/Shared/_Layout_IPageable.cshtml";
}
#* other code bla bla bla *#
#foreach (var device in #Model.Devices)
{
<tr>
<td class="forms-inline">
<form id="entrydelete" method="post" onsubmit="return _formconfirm();">
<button class="btn btn-danger btn-sm" type="submit" asp-page-handler="delete" asp-route-id="#device.Id">
<i class="bi bi-trash3"></i>
</button>
</form>
<form id="useredit" method="post">
<button class="btn btn-warning btn-sm d-inline" type="submit" asp-page-handler="update" asp-route-id="#device.Id">
<i class="bi bi-pencil-square"></i>
</button>
</form>
<form id="toggle" method="post" target="_self">
<button class="btn #(device.Enabled ? "btn-success" : "btn-secondary" ) btn-sm d-inline" type="submit" asp-page-handler="toggleEnabled" asp-route-id="#device.Id">
#if (device.Enabled)
{
<i class="bi bi-toggle-on"></i>
}
else
{
<i class="bi bi-toggle-off"></i>
}
</button>
</form>
</td>
<td>#Html.Label("Description",device.Description ?? "---")</td>
<td class="font-monospace w-space-pre">#Html.Label("Ip",device.Ip)</td>
<td class="font-monospace">#Html.Label("Port",device.Port.ToString())</td>
<td>#Html.Label("Organization",device.OwnedBy?.Name)</td>
<!-- etc. etc. -->
</tr>
}
Now this works great if you have no paging and stuff... But later on as devices grew exponentially a paging solution needed implementation.
So i added an IPageable interface and implemented extension methods and stuff, but i added an additional layout Layout_IPageable.cshtml adding the paging controls at the bottom of the page in the classic
[<-] [1] ... [n-1] [n] [n+1] ... [nmax] [->]
for reusability and style consistency across multiple pages, but i made it a form for general filtering for specific pages
_Layout_IPageable.cshtml
#model IPageable
#{
Layout = ViewBag.BaseLayout ?? "/Views/Shared/_Layout.cshtml";
}
<!-- render body and styles and stuff -->
<div class="paging form text-end">
<label asp-for="Paging.PageNumber" class="form-label"></label>
#if (Model.Paging.PageNumber > 1)
{
<button class="btn btn-sm btn-secondary" onclick="navigatePaging(-1)"><i class="bi bi-arrow-left"></i></button>
}
else
{
<button class="btn btn-sm btn-outline-secondary" disabled><i class="bi bi-arrow-left"></i></button>
}
<!-- etc. etc. -->
[ <input asp-for="Paging.PageNumber" style="width: min-content" form="search" min="1" max="#Model.Paging.MaxPageCount" onchange="this.form.submit()" />
/#Html.Label(Model.Paging.MaxPageCount.ToString()) ]
<!-- etc. etc. -->
<label asp-for="Paging.Take" class="form-label"></label>
<select asp-for="Paging.Take" form="search" onchange="this.form.submit()">
<option>10</option>
<option>25</option>
<option>40</option>
<option>100</option>
</select>
</div>
interface IPageable
{
DataPagingModel Paging { get; set; }
}
public static class PagingExtensions
{
public static IEnumerable<T> Page<T>( this IEnumerable<T> input, DataPagingModel paging )
{
paging.ResultsCount = input.Count();
paging.MaxPageCount = (input.Count()-1) / paging.Take + 1;
paging.PageNumber = Math.Min(paging.PageNumber, paging.MaxPageCount);
return input
.Skip(paging.Skip)
.Take(paging.Take);
}
}
[ValidateAntiForgeryToken]
public partial class DevicesModel : PageModel, IPageable
{
[BindProperty(SupportsGet = true)]
public DataPagingModel Paging { get; set; }
public IActionResult OnGet()
{
thid.Devices = this._devicesSrvc
.VariusChainedLinqMethods()
.Page(this.Paging);
return this.Page();
}
public async Task<IActionResult> OnPostUpdate( Guid id )
{
/* code to update and stuff ... */
// the problem is here, in non-get methods i cannot figure out
// how to return the correct paging because this.Paging is null!
return this.OnGet();
}
/* etc. etc. */
}
The problem is that now when previus "quick actions" forms are submitted, obviusly the paging form is NOT submitted as well, and the paging resets to the default (so page 0, with default page size and no filters).
How should i go about this?
Implement the previus "quick actions" forms as API calls and then reload the page?
Or is there a more elegant solution?

The solutions was pretty simple actually, because the original query is sent by the browser as an URL in the Referer HTTP Header when submitting any form.
So when sending one of the "post" forms, i expect the referer to be the original GET query for the page.
Given it's not something to completely rely on, but returning this at the end of the varius Post handlers...
protected IActionResult RedirectReferOrReload( )
{
var referer = this.Request.Headers.Referer.SingleOrDefault();
if (string.IsNullOrEmpty(referer))
return this.RedirectToPage();
// this ensures the referer cannot go to a malicius link
return this.Redirect(this.Request.Path.Value + new Uri(referer).Query);
}
public async Task<IActionResult> OnPostUpdate( Guid id )
{
/* code to update and stuff ... */
return this.RedirectReferOrReload();
}
... will redirect to the referer page if the header is present, otherwise just reload the page.
When the page is then reloaded with the correct query the status of the shown items is correctly updated.
Now, this works great for my case because i expect default browser configurations from the target users of this page, so that the Referer header is always sent on forms (i also updated ASP.NET to specify a Referrer-Policy).

Related

How to retain search results when clicking browser's back button - ASP.NET Core MVC

I am creating an ASP.NET Core 3 MVC application that has a Customers tab in addition to the Home tab. On the Customers tab there is an input box where the user adds a search criterion (number of days) and a Search button. When the button is clicked then a list of Customer Ids is shown underneath (using jQuery and a Partial View). When the user clicks on a customer Id then the customer information is shown in a different page. However when I click on the browser's back button or on the 'Customers' tab then the criterion added and the search results disappear.
I have tried using the ResponseCache attribute to retain the search results but I could not make it work. I have also tried using the Cache Tag Helper but again was not successful. Anyone can help?
CustomersController
public class CustomersController : Controller
{
private readonly DbContext _context;
public CustomersController(DbContext context)
{
_context= context;
}
public IActionResult Index()
{
return View();
}
public IActionResult DisplayCustomerIdList(string searchText)
{
List<CustomerDetailViewModel> customers = _context.GetAll().ToList();
CustomerIndexViewModel model = new CustomerIndexViewModel()
{
Customers = customers
};
return PartialView("_CustomerIdListView", model);
}
public IActionResult Detail(decimal? Id)
{
Customer customer = _context.GetCustomerById(Id);
CustomerDetailViewModel model = new CustomerDetailViewModel(customer);
return View(model);
}
}
Index.cshtml
#{
ViewData["Title"] = "Customers Page";
}
#section Scripts {
<script type="text/javascript" src="~/lib/jquery/dist/jquery.min.js"></script>
<script>
var url = '#Url.Action("DisplayCustomerIdList", "Customers")';
$('#search').click(function () {
var keyWord = $('#NumberOfDays').val();
$('#searchResults').load(url, { searchText: keyWord });
return false;
})
</script>
}
<body>
<div class="input-group mb-3 w-50">
<input type="text" class="form-control mr-2" placeholder="Number of days" autocomplete="off" id="NumberOfDays">
<button id="search" class="btn btn-outline-info mb-2">Search</button>
</div>
<div id="searchResults"></div>
</body>
_CustomerIdListView.cshtml
#model MyProject.Models.CustomerIndexViewModel
<div class="card border-info mb-3 shadow" style="width:220px; height: 625px; overflow-y: scroll;">
<div class="card-header">Customer Ids</div>
<div class="list-group">
#foreach (CustomerDetailViewModel customerdetails in Model.Customers)
{
<a asp-controller="Customers" asp-action="Detail" asp-route-id="#customerdetails.CustomerId" class="list-group-item list-group-item-action">
#customerdetails.CustomerId
</a>
}
</div>
</div>
Detail.cshtml
#model MyProject.Models.CustomerDetailViewModel
<h3>Customer Information</h3>
<ul>
<li>#Model.CustomerId</li>
<li>#Model.FullName</li>
</ul>
Do the search via a GET request (rather than post). That way, the actual URL the user is sent to includes the query.
<form action="/foo" method="get">
I have figured out why this was not working and thought to add it here in case someone else has the same issue.
It turns out that the jQuery .load() method creates a POST request when the input parameter is an object (and a GET request when it is a String). So, because the ResponseCache attribute does not work with POST requests, the caching was not working.

Passing input value to a controller method, not the whole model in Asp.Net Core 3

I have a ToDo items dashboard page where I display the ToDo's, their status and some other app info.
I want to have one input where I can add a string value (the ToDo title) and on the button click
have that passed to the controllers Create get method, so it populates the Create views Title input field with that value.
I want to it without a form if that is possible as the dashboard page already has a model which is an IEnumerable, just pass that value as a querystring parameter to the Create pages get view (or is it doable in javascript?).
Im not an MVC expert and also not as familiar with the new tag helper methodologies. Any help in how to structure thiswould be helpful.
Here is the html
<!-- Add Task -->
<div class="input-group input-group-lg">
<input class="form-control" type="text" placeholder="Add task and press enter..">
<span class="input-group-addon">
<a asp-controller="ToDoItems" asp-action="Create" ><i class="fa fa-plus"></i></a>
</span>
</div>
<!-- END Add task -->
here is the new model
public Class MyModel{
public IEnumerable<your old model> Old Model {get; set;}
public string Title {get;set;}
}
You can create a form like so in html with razor syntax
#model MyModel
...
<form action="/Controller/PostTitle/" method="post">
#Html.TextBoxFor(m => m.Title,new {#class = "...", #placeholder="...",
#requried="required"})
<input id="export-btn" type="submit" value="Download" class="btn btn-info" />
</form>
The #TextBoxFor will create a textbox and the lambda lets you use your strongly typed model.
Here is the controller
[HttpPost]
public IActionResult PostTitle(string Title) {
...
}

How to get the current Action name inside the View

In my _Layout page, I have got a search form and each controller has an index view. When the user clicks the search button, it searches in the current index view.
I want to show the search field if the user is index view if they go to other views, I wanted to hide it.
In my _Layout
<form asp-action="Index" method="get" class="navbar-form form-inline navbar-right">
<input class="form-control mr-sm-2" id="search" name="search" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" id="BtnSearch" name="BtnSarch" type="submit">Search</button>
</form>
I am using JQuery at the moment but it is quite difficult to put every single view
$("#search").hide();
$("#BtnSearch").hide();
Basically, in my _Layout page, I wanted to show or hide Search form if the user is in the index view.
how can i get current view name in _Layout view, please?
Basically, in my _Layout page, I wanted to show or hide Search form if the user is in the index view.
Try with below codes :
#if ("Index".Equals(ViewContext.RouteData.Values["Action"].ToString()))
{
<form asp-action="Index" method="get" class="navbar-form form-inline navbar-right">
<input class="form-control mr-sm-2" id="search" name="search" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" id="BtnSearch" name="BtnSarch" type="submit">Search</button>
</form>
}
This sounds like it is the ideal candidate for mvc tag helpers.
You will need to create a class which inherits from TagHelpers and override the process method.
[HtmlTargetElement(“website-search”)]
Public class Search : TagHelper
{
Public WebsiteContext Info { get; set; }
Public override void Process(TagHelperContext context, TagHelperOutput output)
{
Output.TagName = “section”;
Output.Content.SetHtmlContent(“ HTML for your search form “);
Output.TagMode = TagMode.StartTagAndEndTag;
}
}
In order to get the controller and action you will need to add a property to the tag helper:
[HtmlAttributeNotBound]
[ViewContext]
Public ViewContext ViewContext { get; set; }
Now that you have the view context in place, you can look to do something like the following:
If(ViewContext.RouteData.Values[“action”]) != “Index”)
{
Output.SuppressOutput();
}
You can then reference this by putting website-helper in your view.
Please see the following link for an intro on tag helpers https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.2
There is the following stack overflow question detailing how to get the controller and action executed against : Knowing what route the user is on inside TagHelper in .NET Core
Hope this helps
You can add a hidden input to layouts file and assign to it an id.
Then you can get action and controller name from anywhere:
<input type="hidden" value="#this.ViewContext.RouteData.Values["action"].ToString()" />
<input type="hidden" value="#this.ViewContext.RouteData.Values["controller"].ToString()" />
So if you don't use them in JS, you can declare a variable and show your form when action is Index.
Hope to help.
If you want to show the search form only in specific views, I would not base this on the view name. In the future, you might also need it in other views. So, why not simply add a flag to show the search form to your ViewBag. It will mean, setting this flag in every "Index" action, but you will be more flexible with where to show it.
Controller:
public ActionResult Index()
{
this.ViewBag.ShowSearch = true;
// … your normal code
return this.View();
}
_Layout.cshtml
#if (this.ViewBag.ShowSearch == true) // explicitly check for true, so not having set the flag in the ViewBag will not pose a problem, i.e. null != true.
{
<form action="">#* … *#</form>
}

Model from PartialView to Controller returns null

I have a PartialView that recibes a Model ID and then when you click on the link sends the Model ID to the Controller. My problem was that there were 2 ways to send data from View to Controller but just only one worked for me. What's the reason why?
This worked perfectly for me
<a type="button" class="btn btn-primary" asp-controller="Dictionary" asp-action="Edit" asp-route-wordId="#Model"><i class="fas fa-edit"></i></a>
This one didn't work
<a type="button" class="btn btn-success" href="#Url.Action("Edit/" + Model)"><i class="far fa-list-alt"></i></a>
My question is why the last one didn't work and returned null if I am passing the Model ID to the URL
This is my full PartialView that receives a Model ID and works
#model int
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<td style="width:150px">
<div class="btn-group" role="group">
<a type="button" class="btn btn-primary" asp-controller="Dictionary" asp-action="Edit" asp-route-wordId="#Model"><i class="fas fa-edit"></i></a>
</div>
</td>
And this is my Controller Function that recibes the data
public async Task<IActionResult> Edit(int? wordId)
{
if(wordId == null)
{
return NotFound();
}
var word = await _db.Dictionaries.FindAsync(wordId);
if(word == null)
{
return NotFound();
}
return View(word);
}
The Url.Action overload you are using takes in an action name and you are passing something like /Edit/4 to it. You need to use the proper overload of the Url.Action method that allows you to specify route values.
It would look something like the following:
<a type="button" class="btn btn-success" href="#Url.Action("Edit/" + Model)"><i class="far fa-list-alt"></i></a>
to
<a type="button" class="btn btn-success" href="#Url.Action("Edit", new { wordId = Model } )"><i class="far fa-list-alt"></i></a>`
As a side note, your Edit action takes a nullable wordId which doesn't make much sense to me. Generally, if you're editing something then it has already been created and should always have an id. The only case I can think of where this makes sense is if the Edit action actually handles creating data as well as editing data in which case that would be perfectly valid.
To answer your main question, the reason your 1st anchor is working is because the args are correct.
For example:
<a asp-controller="Dictionary" asp-action="Edit" asp-route-wordId="1">Your link</a>
is equivalent to:
That works!
The second will not work because you are misusing Url.Action, the method you are using is this Url.Action("Action")
Url.Action("Edit/" + Model) or Url.Action("Edit/1")
is searching for an action named: "Edit/1", which in your case will not be found.
so in your 2nd not working example
Your Link
is equivalent to:
<a href="null" />
In your case you should be using this in your anchor:
Url.Action("Edit", "Dictionary", new { id = #Model })
is equivalent to
/Dictionary/Edit/1
so:
Your link

Showing message in a razor view based on a result from execution of controller method

I'm working on asp.net mvc 3 application. I'm implementing a razor view which have tow main functions - to build/display form based on a data from a data base and to show images related to this form in a custom (made by me) image gallery that allows upload and delete of image.
So generally this is my view with both forms for visualizing the form and showing and uploading image(s) :
#model List<DataAccess.MCS_DocumentFields>
#{
ViewBag.Title = "Документ";
}
<div id="alabala">
<div id="drawForm">
#using (Html.BeginForm("UpdateDocument", "Forms", FormMethod.Post))
{
<table border="1" id="drawDocument">
<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>
#Html.Partial("_PartialHeader", Model)
#Html.Partial("_PartialDrawing", Model)
#Html.Partial("_PartialBody", Model)
#Html.Partial("_PartialFooter", Model)
</table>
if (ViewBag.Status == 1)
{
<button type="submit" id="submitDocument">Запази</button>
<button style="float:right;" id="finalizeDocument">Приключи</button>
}
else
{
#Html.ActionLink("Назад", "Index")
}
}
</div>
<div id="imageContainer">
<div id="imageGallery" style="overflow: scroll">
<img src="file:\\..." alt="docImg" style="width: 190px; height: auto"/>
#Ajax.ActionLink("Delete", "DeletePicture", new { documentID = Model[0].Id },
new AjaxOptions
{
Confirm = "Are you sure?",
OnComplete = "$('#blah').attr('src', '#').attr('style', 'display:none;'); $('#Image1').attr('src', '#').attr('style', 'display:none;'); $('#DelPic').attr('style', 'display:none;');"
})
<img src="file:\\..." alt="docImg" style="width: 190px; height: auto"/>
#Ajax.ActionLink("Delete", "DeletePicture", new { documentID = Model[0].Id },
new AjaxOptions
{
Confirm = "Are you sure?",
OnComplete = "$('#blah').attr('src', '#').attr('style', 'display:none;'); $('#Image1').attr('src', '#').attr('style', 'display:none;'); $('#DelPic').attr('style', 'display:none;');"
})
</div>
#using (Html.BeginForm("Upload", "Forms", FormMethod.Post))
{
<input name=#Model[0].DocumentId type="hidden" />
<input type="file" name="datafile" id="file" onchange="readURL(this);" />
<input type="button" name="Button" value="Upload" id="UploadButton" onclick="fileUpload(this.form,'/forms/upload','upload'); return false;"/>
<div id="upload" style="display: inline-block;">
<img id="blah" src="#" alt="your image" style="display:none;"/>
</div>
}
</div>
</div>
The first form is where I show the data for a current form/document and because they are editable I have submit button. I need the second form to submit the selected picture to my controller and there to perform the business logic.
So once a picture is selected and Upload button is clicked I get to my controller :
public ActionResult Upload(FormCollection collection)
{
WebImage UploadImage = WebImage.GetImageFromRequest();
long documentID;
string finalImageName = null;
if (!long.TryParse(collection.AllKeys[0], out documentID))
//More code...
Where I have the image and the id of the document that it belongs to and what I need is to perform some checks/validations and finally to coy the selected image to dedicated directory and save the name to the data base.
The problem is that I have all the logic written except the one that will show the correct messages for the different outputs like :
if (imagePath.Length > 247)
{
//TODO message that the path is too long
//TODO this return View() is temp, replace with something suitable
return View();
}
//...
System.IO.File.Copy(UploadImage.FileName, imagePath);
}
catch (Exception ex)
{
//TODO copy failed return message
return View();
}
//...
These are all different outputs from the execution of the same method and in the main view I want to show a proper message for each one of them. What I'm not sure is if I still have an option to save my work and still implement the message logic? Thinking now it seems that if I was using Ajax in some form it would be a lot easier now but I'm not. The only think I can think of know is creating ViewBag property, and returning it with the model to the view where to check the different properties and show some message if necessary based on that, but this means a lot of additional logic in my view, resending data from the database that I already have shown in my view and a lot of double work said in short, something that I consider as a bad programming, but maybe I got myself into this. So what is the best course of action from here on. Is it best to just remove my code and search for a way to do this with AJAX?
You can't upload a file using AJAX - you'd need some sort of 3rd party workaround. You need to return the original view from the Upload() method, with the appropriate model, and also a flag within the ViewBag somewhere to display the message, e.g.
public ActionResult Upload(UpdateDocumentModel model) {
...
if (imagePath.Length > 247) {
model.ErrorMessage = Errors.Over247;
return View("UpdateDocument", model);
}
...
return RedirectToAction("UploadOk");
}
I've changed your FormCollection to a strongly-typed model for ease of reading, plus that's what MVC.net is there for. The Errors.Over247 could be a string resource somewhere in your project, or a boolean flag which the View then reads to show a certain piece of HTML.
just use TempData instead of ViewBag
TempData["ErorrMessegge"] = "SomeMessage to view";
#TempData["ErorrMessegge"]
You can simply use TempData[] in order to pass parameter from Controller to View like below:
Controller:
[HttpPost]
public ActionResult Add(Applicant applicant)
{
repository.SaveApplicant(applicant);
TempData["message"] = "The applicant has been saved succesfully.";
return View(applicant);
}
View:
#if (TempData["message"] != null)
{
<div>#TempData["message"]</div>
}

Categories

Resources