I'm trying to figure out using AJAX with Razor Pages.
I've been searching the Web but each example I've found does something different, and most are incomplete or not for Razor Pages.
So far, I've been focusing on variations of something like this:
$.post('/?handler=Delete', 5, function (x) {
alert(x);
});
And then my page model looks like this:
public void OnPostDelete(int id)
{
}
I've tried variations on this but, so far, my C# code is not getting called.
Questions:
Can someone show me what I'm missing?
Can anyone offer some good references for this? (I need to perform other AJAX tasks as well.)
Some examples I found had special handling related to anti-forgery tokens. Do I need to code for that as well?
UPDATE:
So I've been working with this and this is what I have now:
$.ajax({
url: '?handler=Delete',
data: {
id: $(this).data('id')
}
})
.fail(function (e) {
// Error
alert(e.responseText); // Way too much info
})
.done(function () {
// Success
})
.always(function () {
// Always
});
And my handler:
public void OnGetDelete(int id)
{
}
This is in fact calling my handler and I finally got it to pass the id argument.
Since I have a bounty, here's what I'd like to see in an answer:
If I set the AJAX call to use POST and rename my handler to OnPostDelete(), the handler is not called. How would I do a post?
Any other suggestions or criticisms with the code above? I know there are many ways to do this. I'm just looking for the simplest way and trying to refine it.
You could pass F12 to check the request in Network tab , you may see 400 bad request error.
Razor Pages are designed to be automatically protected from cross-site request forgery (CSRF/XSRF) attacks. You don’t have to write any additional code. Antiforgery token generation and validation is automatically included in Razor Pages. Here the request fails, there is no AntiForgeryToken present on the page.
For the question , you could add explicitly using #Html.AntiForgeryToken()
To add AntiForgeryToken, we can use any of the approaches. Both the approaches add an input type hidden with name __RequestVerificationToken. The Ajax request should send the anti-forgery token in request header to the server. So, the modified Ajax request looks like,
#Html.AntiForgeryToken()
#section Scripts
{
<script>
$.ajax({
type: "POST",
url: "/?handler=Delete&id="+5,
beforeSend: function (xhr) {
xhr.setRequestHeader("XSRF-TOKEN",
$('input:hidden[name="__RequestVerificationToken"]').val());
},
success: function (x) {
alert(x);
},
failure: function (response) {
alert(response);
}
});
</script>
}
Since the script sends the token in a header called X-CSRF-TOKEN, configure the antiforgery service to look for the X-CSRF-TOKEN header:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
}
Reference:https://www.talkingdotnet.com/handle-ajax-requests-in-asp-net-core-razor-pages/
This is how I like to do it:
Configure your Javascript
//If you're returning a object, configure it
var yourObject = {
field1: "value1",
field2: "value2"
};
//setup ajax
$.ajax({
data: yourObject,
type: "POST",
url: "urltoyourhandler/delete" //you can add other paramters here as well by doing ?parm=value
success: function(data){
//do success stuff here
//based off my handler code below:
if(data.success){
console.log("Success!");
}
else{
console.log("Failed for some reason!");
}
}
error: function(){
//do error stuff here
//gets called if there is a issue contacting the URL or if there is a server error like 500
}
});
Configuring your handler. For my CRUD operations, I like making a CRUD controller to handle everything
[BindProperty]
public YourClass Name { get; set; }
//set handler to only accept POST and set a URL for it. URL should be to the same folder you're in
//the 'delete' in route doesn't have to match the function name but it's less confusing if it does
[HttpPost, Route("RouteToThisHandler/delete)]
public async Task<IActionResult> Delete()
{
//verify data and the do something with it
//I like returning a JsonResult. Add whatever data you want. I like returning success
//with true or false and some other data if needed
return new JsonResult(new { success: true, importantInfo: "This is important" });
}
Ajax has more configuration options to give you more information about any server errors that occur
As for the anti-forgery token, Microsoft says:
Antiforgery middleware is added to the Dependency injection container
when one of the following APIs is called in Startup.ConfigureServices:
AddMvc
MapRazorPages
MapControllerRoute
MapBlazorHub
Here is a Microsoft link about the anti-forgery token: https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-3.1
I investigated a few things, what happens when you pass a string from AJAX instead of an integer and how the different routes affect the call (see interesting note below if you've got time) But mostly, all I found is that Razor Pages are pretty forgiving and everything seemed to just work. Like I mentioned, even passing a string where an integer type id still hit the handler method (it just gave me a default(int))
I created this repo just for exploring: https://github.com/EntityAdam/RazorPageHandlers
The major blocker, as #Yan pointed out is the Anti-Forgery token. That is really the only thing that caused a handler not to hit a breakpoint. As was suggested, check your network tab for failed requests or the console for bum JavaScript code.
From your snippet, to change it to a OnPostDelete you would need to use the POST type in the AJAX call AND include the Anti-Forgery Token.
$.ajax({
type: 'POST',
headers: { "RequestVerificationToken": $('input[name="__RequestVerificationToken"]').val() },
url: '?handler=Delete',
data: {
id: $(this).data('id')
}
})
.fail(function (e) {
// Error
alert(e.responseText); // Way too much info
})
.done(function () {
// Success
})
.always(function () {
// Always
});
Another topic that I didn't see discussed is where is this token coming from? It's generated by Razor automagically whenever there is a <form> element. If you do not have a <form> element, you can generate tokens when you need them, check out the #functions {} block in this MSDN Article. There are also scenarios where CSRF is useless or not required and you could also turn anti-forgery off if you don't need it (That's an entirely different discussion).
My criticisms of the approach are opinions so take 'em or leave 'em.
Don't use jQuery if you can avoid it based on the premise that improvements to JavaScript have arguably obsoleted jQuery. Use plain vanilla JS code.
Ajax is showing it's age as well. If you only need to support modern browsers consider the Fetch API
Don't write any JS at all! Blazor is new and shiny. There are still some growing pains, but I'd rather my pain be in C# than in JavaScript =)
An interesting thing I came across..
For this demo, I used the following #page directive
#page "{id?}"
In the HTML part of the forms like:
<form method="post" asp-page-handler="Delete">
<input type="hidden" name="id" value="1" />
<button class="btn btn-danger">Delete</button>
</form>
I'm using the asp-page-handler tag helper to assist in generating the correct URL. For the Create handler, with that #page directive the tag helper comes up with a form target of /?handler=Create
If you swap out that #page directive with #page "{handler?}/{id:int?}", the tag helper figures out the route which is now /Delete. But guess what? The AJAX calls work with either #page directive, even though the URL in the AJAX is hard coded to ?handler=Delete'
Two suggestion:
1- add the page path in front of the url. (I am not sure if it is mentioned in your words).
$.post('/{page_path}/?handler=Delete', 5, function (x) {
alert(x);
});
2- Follow the route map rule. For example, there is the 'id' in your function. The page may have configured like #page "{id:int}". So the url should be something like
$.post('/{page_path}/{id}/?handler=Delete', 5, function (x) {
alert(x);
});
Related
Recently, I read about request handling in asp.net core razor pages and it says that it supports head requests using the convention:
public void OnHead()
{
}
It worked perfectly. And then I also tried delete using the same convention.
public void OnDelete()
{
}
But when I send a delete request using postman, it returns a bad request (500). I'm not sure if i need to provide additional configurations to use delete requests. Any one could help me pls.
There is no OnDelete/OnPut. This is because Razor Pages are directly geared towards web views, i.e. pages displaying in a browser tab/window. There is no native way in a browser to send DELETE/PUT requests, so there's no reason to support them. Instead, such tasks are handled via an HTML form element, which would send via POST. As such, you would use OnPost() to handle it.
The docs recommend creating a new Razor Page for delete, with its own OnGet and OnPost methods, specifically geared to handling deletes. Alternatively, you may simply add an additional handler to an existing Razor Page in the form of OnPost[Something]. For a delete, that would likely be OnPostDelete, while for an update, you'd likely have OnPostUpdate. The name doesn't matter, except that you will need to pass it as a handler such as:
<form asp-page="Foo" asp-handler="Delete">
If you need to interact via a thin client (HttpClient, AJAX, Postman, etc.), then you should avoid Razor Pages altogether and stick with traditional controllers, which fully supports all HTTP verbs.
Assuming you have a markup like this:
<button type="submit" asp-page-handler="delete"
asp-route-id="#contact.Id">delete</button>
The correct method for delete would be OnPostDelete (or OnPostDeleteAsync).
So, could update to:
public void OnPostDelete(int id)
{
}
The docs state:
By convention, the name of the handler method is selected based on the
value of the handler parameter according to the scheme
OnPost[handler]Async
Further, the Async suffix is optional:
The Async naming suffix is optional but is often used by convention
for asynchronous functions.
This is an update for 2023.
You can easily utilize the DELETE verb of an Ajax request:
public async Task<IActionResult> OnDeleteAsync(int id)
{
await _myService.DeleteUser(id);
return new OkResult();
}
With jQuery it looks something like this:
<script>
$(".deletebtn").on("click", function (e) {
deleteUser($(this).data("id"));
});
const deleteUser = function (userId) {
$.ajax({
method: 'DELETE', // specify HTTP verb.
url: '/account/users', // specify right path to your controller/razor page
headers:
{
"RequestVerificationToken": "<get your token here>"
},
data: { id: userId },
success: function () {
window.location.href = "/account/users"; // reload page
},
error: function (response) {
alert(response);
}
});
};
</script>
<button class="deletebtn" data-id="123">Delete</button>
<button class="deletebtn" data-id="456">Delete</button>
<button class="deletebtn" data-id="1111">Delete</button>
Similarly the HTTP verbs GET, POST, PUT can also be utilized:
OnGet(), OnPost(), OnPut()
and their async equivalents
OnGetAsync(), OnPostAsync(), OnPutAsync()
Usage e.g.:
OnGetUsersAsync() // GET a list of Users async.
OnPostUserAsync() // POST (add) a User async.
OnPutUpdateMyUserAsync() // PUT (update) a user async
OnDeleteCoffeCup(id) /// DELETE method sync.
In addition you can add your own experimental HTTP verbs. See e.g. here https://khalidabuhakmeh.com/adding-experimental-http-methods-to-aspnet-core
Allow me to preface this by saying that I did look into other SO questions dealing with this matter, but they were either ASP solutions or not quite exactly what I needed.
My question is as follows, can I make an Http Post request to a method in my controller that returns a partial view if I am using Html.BeginForm? I know, and have used, POST methods using BeginForm multiple times, however, those were to ActionResult methods which were responsible for transferring the data stored over into the server. What I would like to do is something as follows:
Say I have this cshtml:
<div class="test">
#using (Html.BeginForm("MyPartialViewMethod"), FormMethod.POST) {
#Html.LabelFor(x => x.StartTimeLabel, Model.StartTime);
#Html.TextBoxFor(x => x.StartTime);
<input type="button" value="Submit"/>
}
</div>
Such that in my Controller I could say have the following example method.
[HttpPost]
public PartialViewResult MyPartialViewMethod(Model DynamicDataModel) {
//Pass the data in the DynamicDataModel into a new cshtml page and
//have the partial view it returns be rendered on screen
}
So in a nutshell:
Make POST request to server passing into it my data values
Have server process the data and return the partial view accordingly populated with all the required data.
I imagine that I could take the approach I used to do back in college with php which is AJAX call via JS to php, php spits out html, JS adds the html where it is needed. However, this seems to be a bit too rough.
yes you can.
you can also do it in ajax with a custom submit buton who call your ajax an jquery,
with something like that and serialize your model.
or just take the controller part if no ajax and submit
$.ajax({
url: '#Url.Action("actionName", "controllerName")',
data: $('#MyPartialViewMethod').serialize(),
dataType: "json",
type: 'POST',
success: function (data) {
$('html').html(data);
},
error: function (request, error) {
}
});
[HttpPost]
public actionName(Model myModel)
{
do thing with your model
return PartialView(myModel);
}
an loop
I have a View which is the container for a PartialView. Let's say a Customer - Orders relation. The View should received a CustomerViewModel whereas the PartialView a collection of Orders, such as IEnumerable<OrderViewModel>.
I basically have two ways of doing this (not to mention Angular), either Razor or jQuery. With Razor was pretty straightforward by utilizing #Html.Partial("_CustomerOrdersPartial", Model.Orders). But let's assume I cannot use Razor syntax and here it is how I ended up posting this question. I have read many posts on this matter but, most of them (not to mention all), suggest to use $("#container").load('#Url.Action("ActionName", new { parameterX = valueY })). Then here are my questions:
Why to mix Razor and jQuery?
Is this the only way?
Is there any way to call the View and pass the model?
The last question has to do with the fact that the above code requires an action on the server-side to be called, whereas the #Html.Partial("_CustomerOrdersPartial", Model.Orders) mentioned above will just call the View (client-side) and send the given Model in.
Any idea on how to solve this would be really helpful.
Thanks in advance for your time and thoughts.
my solution is:
function ReturnPanel(div, panel) {
$.ajax({
type: "POST",
url: "#Url.Action("GetPanel", "ControllerName")",
data: JSON.stringify({ 'idCurso': idCurso, 'panel': panel }),
contentType: 'application/json; charset=utf-8',
success: function (response) {
$("#" + div).html(response);
},
error: function (xhr, status, errorThrown) {
//Here the status code can be retrieved like;
alert("Error: status = " + xhr.status + " Descripcion =" + xhr.responseText);
}
})
}
in cs.
[HttpPost]
public ActionResult GetPanel(int idCurso, string panel)
{
Contenido contenido = new Contenido();
contenido.IdCurso = idCurso;
return PartialView(panel, contenido);
}
This code should do it. The trick is to acquire the URL and then make sure you get the parameter list right. I used a little Razor to get the URL but you don't have to. Also, If you fail to match the parameter list, your call will not even be acknowledged. You have been warned. I tried to name every thing in a way that helps.
var url = '/controllerName/ActionName';
$('#pnlFillMee').load(url, {NameOfParam: $('#elementID').val() },
function () {CallMeAfterLoadComplete(); });
Here's a real world example I use at work. ReviewInfo is an action in the
controller associated with this page. It returns a partialview result.
$(document).ready(function () {
var url = '/supervisor/reviewinfo';
$('#pnlReviewInfo').load(url, { FCUName: $('#FCU').children(':selected').text(), AccountsFromDate: $('#AccountsFrom').val()}, function () {
InitializeTabs(true);
});
});
This goes somewhere on your form.
<div id="pnlReviewInfo" style="width: 85%"></div>
EDIT:
I would also look up the other jQuery functions like $.get, $.post and $.ajax which are more specialized versions of $.load. and see this link which might answer all your questions about passing models:
Pass Model To Controller using Jquery/Ajax
Hope this helps
wrapping up this question and thanks to #stephen-muecke and #charles-mcintosh for their help:
Using #Html.Partial(partialViewName) the server returns a string resulting from the partial view passed in. Preferred method if you need to manipulate the partial before being displayed. Otherwise, using #Html.RenderPartial(partialViewName) will write into the stream output sent to the browser, the HTML code from the given partial.
As per jQuery API, $(elem).load(url[,data][,complete]) will place the returned HTML into the matching element. Thus, it requires an action method for the given url.
If for whatever reason Razor cannot be used on the UI, chances are you would likely end up either hard-coding the url like in the sample code provided above by #charles-mcintosh or using Angular.
My app has a "Show all comments" similar to the one in Facebook. When the user clicks on the "show all" link, I need to update my list which initially has upto 4 comments with all comments. I'll show some code first and then ask some questions:
jQuery:
ShowAllComments = function (threadId) {
$.ajax({
type: "POST",
url: "/Home/GetComments",
data: { 'threadId': threadId },
dataType: "json",
success: function (result) {
alert(result);
},
error: function (error) {
alert(error);
}
});
};
Home Controller:
// GET: /GetComments
[HttpPost]
public JsonResult GetComments(int threadId)
{
var comments = repository.GetComments(threadId).ToList();
return Json(comments );
}
Questions:
When I tried GET instead of POST, I got this error: "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet." Is POST usually recommended instead of GET when making these ajax requests? If not, how do I get it to work with GET? Where do I set JsonRequestBehavior to AllowGet?
After changing it to POST, I now get this error: A circular reference was detected while serializing an object of type 'Models.Thread'. I'm using Entity Framework 4, and I've read that adding a [scriptignore] attribute on the navigation property would resolve this, so I added a partial class of the entity with the property, it said "property is already defined". How do I then deal with this because I can't directly modify the code generated by EF4.
Set in the return Json. I would just use post, but if you want to make it hard on your self, use get.
public JsonResult blah()
{
return Json("obj", JsonRequestBehavior.AllowGet);
}
It is true when most ORM objects get serialized the serialization tries to searlize the hidden goodies the ORM needs, AND (sounds like your case) all of the lazy load stuff... this causes bad mojo. Can I throw a suggestion out? Why not let MVC do what its good at and generate the view for you? You could just use the jQuery .load and use a view.
Answers:
try return Json(comments, JsonRequestBehavior.AllowGet); There are good reasons for the default behavior though.
for anything going down the wire, you are going to really want to create a very simple view model object rather than sending your domain entity down the wire. Lots of fancy things happen in the EntityFramework that don't work with serialization as you are finding out.
I have this jQuery get request:
$.get($(this).attr("href"), { "searchExpression": "schroders" }, function (result) {
// do stuff
}, "html");
Which sends the get request to this Action Method:
public PartialViewResult Tabs(string searchExpression)
{
return PartialView(new SearchViewModel
{
PagedFunds = _fundService.GetFunds(searchExpression)
});
}
The $.get request sends a request to the Tabs method, but searchExpression is always an empty string. I've done this before and it's worked.. does anyone have any idea why either the data isn't being sent or the Model Binder isn't working?
edit: I've just discovered the version of jQuery being used is 1.2.6. Also, There's another JS framework being used on the site - Prototype, I think - so this is the complete function that I'm using for the GET, which manages the compatibility issues:
jQuery(document).ready(function ($) {
$('.ActionControl a').click(function () {
$.get($(this).attr("href"), { searchExpression: "schroders" }, function (result) {
// do stuff
}, "html");
return false;
});
});
does this offer any clues? Thanks
Did you debug it for the get parameter by firebug?
See the params that you are sending through ajax.
If not solved than let me know.
Dave - could be that you need to explicitly decorate the action - i.e.:
[AcceptVerbs(HttpVerbs.Get)]
to ensure that it's not looking for a put/post request etc. I had wondered about the 'html' part previously, but that would only be related to the return type.
see comment above - but to put it in the answer:
dave - i did a test on this in a little test app with jquery 1.4.2 and it worked perfectly. it could be related to the version in use and/or the clash with prototype. you might have to alias the $.get function to jQuery.get (i think that's the syntax)
see http://docs.jquery.com/Using_jQuery_with_Other_Libraries
jim
It turns out this is the the answer to the real issue is outlined here:
How to send jQuery $.get so the Model Binder can bind a string to a parameter?