Make a form submitting both GET and POST - asp.net-core

I have a page, where I do a search. Then I can make some actions on results of the search.
Here the page:
#page "{handler?}"
#model Pricelists.ListModel
<form>
<div><label><input autocomplete="off" type="checkbox" asp-for="OnlyEnabled" /> Only enabled</label></div>
//all other filters used in the search
<input type="submit" formmethod="get" asp-page-handler="Search" />
#if (Model.SearchResult.PaginatedResults != null)
{
<input type="hidden" asp-for="PageNumber" value="#Model.PageNumber" />
<input type="hidden" asp-for="TotalPages" value="#Model.TotalPages" />
<table>
<tr>
//header result
</tr>
#foreach (var item in Model.SearchResult.PaginatedResults)
{
<tr>
//result
</tr>
}
</table>
<label>Page #Model.PageNumber of #Model.TotalPages (Number of records: #Model.SearchResult.Count)</label>
<input type="submit" formmethod="get" asp-page-handler="Previous" value="<" />
<input type="submit" formmethod="get" asp-page-handler="Next" value=">" />
<div>
<input type="submit" formmethod="post" asp-page-handler="Enable" value="Enable" />
<input type="submit" formmethod="post" asp-page-handler="Disable" value="Disable" />
<input type="submit" formmethod="post" asp-page-handler="Delete" value="Delete" />
</div>
}
</form>
As you can see, either when I do the search or when I change the page, I submit the form using GET.
But I would like to make a POST submit to modify (enable, disable or delete) the result of the search.
Here the PageModel:
public class ListModel : PageModel
{
private const int RECORDS_PER_PAGE = 20;
private readonly AdminApiClient _client;
#region Selected Filters
[BindProperty(SupportsGet=true)]
public bool OnlyEnabled { get; set; }
[BindProperty(SupportsGet = true)]
public long[] SelectedCategories { get; set; }
[BindProperty(SupportsGet = true)]
public string[] SelectedCarriers { get; set; }
[BindProperty(SupportsGet = true)]
public string[] SelectedDepartures { get; set; }
[BindProperty(SupportsGet = true)]
public string[] SelectedArrivals { get; set; }
[BindProperty(SupportsGet = true)]
public int PageNumber { get; set; }
[BindProperty(SupportsGet = true)]
public int TotalPages { get; set; }
#endregion
[BindProperty(SupportsGet = true)]
public PriceListSearchViewModel SearchResult { get; set; }
public ListModel(AdminApiClient clientFactory)
{
_client = clientFactory;
}
public async Task OnGet()
{
await this.LoadViewDataAsync();
}
public async Task OnGetSearch()
{
this.PageNumber = 1;
await this.LoadViewDataAsync();
this.SearchResult = await this._client.Pricelists.Search(this.SelectedCategories, this.SelectedCarriers, this.SelectedDepartures, this.SelectedArrivals, this.OnlyEnabled, this.PageNumber, RECORDS_PER_PAGE);
this.TotalPages = (int)Math.Ceiling(this.SearchResult.Count / (decimal)RECORDS_PER_PAGE);
}
public async Task OnGetNext()
{
...
}
public async Task OnGetPrevious()
{
...
}
public async Task OnPostEnable()
{
await this.LoadViewDataAsync();
await this._client.Pricelists.ChangeStatus(this.SelectedCategories, this.SelectedCarriers, this.SelectedDepartures, this.SelectedArrivals, this.OnlyEnabled, this.PageNumber, RECORDS_PER_PAGE, enable: true);
}
public async Task OnPostDisable()
{
await this.LoadViewDataAsync();
await this._client.Pricelists.ChangeStatus(this.SelectedCategories, this.SelectedCarriers, this.SelectedDepartures, this.SelectedArrivals, this.OnlyEnabled, this.PageNumber, RECORDS_PER_PAGE, enable: false);
}
public async Task OnPostDelete()
{
await this.LoadViewDataAsync();
await this._client.Pricelists.Delete(this.SelectedCategories, this.SelectedCarriers, this.SelectedDepartures, this.SelectedArrivals, this.OnlyEnabled, this.PageNumber, RECORDS_PER_PAGE);
}
}
To semplify I let you see just one GET and one POST method. But, in any case I use same binded properties.
Now, when I make a GET submit everything works correctly. But when I make a POST submit I get an error 400 Bad request. I let you see a fiddler:
The http request is what I expected, but obviously not the response.
Any idea?
EDIT
I never go in the methods OnPost*. It is like the way I have implemented the submit button is wrong. The problem is not the implementation of the OnPost* methods.
Thank you

You have two options, Separate forms and write a form for each method. or jsut handle it using javascript.
look at this sample:
<form id="frm" action="/test" method="post">
<button type="submit">save</button>
and script:
<script>
setTimeout(function () {
var frmElement = document.getElementById('frm');
frmElement.setAttribute('method', 'get')
}, 2000)
</script>

try to remove form that you have and make a special form for each button
<form asp-page-handler="delete" method="post">
<button class="btn btn-default">Delete</button>
</form>
.... and so on

You can try to add method="post" to <form>.Example:<form method="post">.
I test with your code,when click the inputs with formmethod="post",it will lose __RequestVerificationToken in the form data.And after you add method="post" to form,it will have a hidden input as the following shows:
result:

Related

EventCallback - is it a func delegate?

What does this mean?
public EventCallback<Trail> OnSelected { get; set; }
Does this mean OnSelected is a delegate (function paramter) that holds input parameter of type Trail and return parameter void?
Why is EventCallback used?
If I have to return a parameter of type string for this delegate how would this declaration look like?
will it look like ?
public EventCallback<Trail, string> OnSelected { get; set; }
EventCallback is a bound event handler delegate.
One of the most common scenarios for using EventCallback is to pass data from a child component to the parent component.
Here is a simple demo about how to pass the string value:
child component
<h3>TestChild</h3>
<input #onchange="UseEcb"/>
#code {
[Parameter]
public EventCallback<string> RecoverRequest { get; set; }
async Task UseEcb(ChangeEventArgs e)
{
await RecoverRequest.InvokeAsync(e.Value.ToString());
}
}
parent component
page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<h2>#result</h2>
<TestChild RecoverRequest="Test"></TestChild>
#code {
[Parameter]
public string result { get; set; }
private void Test(string a)
{
result = "Child Component value is "+a;
}
}
Demo
To answer your first three questions:
An EventCallback is a readonly struct. It's a wrapper for a delegate that supports async behaviour through EventCallbackWorkItem.
It looks like this (extracted from the AspNetCore source code):
public readonly struct EventCallback<TValue> : IEventCallback
{
public static readonly EventCallback<TValue> Empty = new EventCallback<TValue>(null, (Action)(() => { }));
internal readonly MulticastDelegate? Delegate;
internal readonly IHandleEvent? Receiver;
public EventCallback(IHandleEvent? receiver, MulticastDelegate? #delegate)
{
Receiver = receiver;
Delegate = #delegate;
}
public bool HasDelegate => Delegate != null;
internal bool RequiresExplicitReceiver
=> Receiver != null && !object.ReferenceEquals(Receiver, Delegate?.Target);
public Task InvokeAsync(TValue? arg)
{
if (Receiver == null)
return EventCallbackWorkItem.InvokeAsync<TValue?>(Delegate, arg);
return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg);
}
public Task InvokeAsync() => InvokeAsync(default!);
internal EventCallback AsUntyped()
=> new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate);
object? IEventCallback.UnpackForRenderTree()
=> return RequiresExplicitReceiver ? (object)AsUntyped() : Delegate;
}
You can see the above source code and other related code here - https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/EventCallback.cs
To answer your last two questions:
In your example Trail is what you return.
You would call an EventCallback that returns a string like this in the component:
<div class="row">
<div class="col-auto">
<input class="form-control" type="text" #bind="#this.enteredValue" />
</div>
<div class="col-auto">
<button class="btn btn-primary" #onclick=this.HandleSelect>Set Me</button>
</div>
<div class="col-auto">
<button class="btn btn-secondary" #onclick=this.SetSelect>Set Me To Hello</button>
</div>
</div>
<div class="p-2 m-2 bg-dark text-white">
Value: #this.Value
</div>
#code {
private string enteredValue = string.Empty;
[Parameter] public EventCallback<string> OnSelected { get; set; }
[Parameter, EditorRequired] public string Value { get; set; } = string.Empty;
private async Task SetSelect()
{
await OnSelected.InvokeAsync("Hello");
}
private async Task HandleSelect()
{
await OnSelected.InvokeAsync(enteredValue);
}
}
And consume it like this:
#page "/"
<h2>Test Page</h2>
<MyComponent Value=#this.textValue OnSelected=this.HandleValueChanged />
#code {
private string textValue = string.Empty;
private async Task HandleValueChanged(string value)
{
// Emulate some async activity like getting data
await Task.Delay(1000);
this.textValue = value;
}
}
If you want to return more complex data, create a struct or record to return.
For general usage see the MS-Docs article - https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-6.0#eventcallback.

make a small change to a large Blazor App

This is a followup to a similar question I had but this time with a working example below. This app re-renders/re-calculates the entire app--even though only a tiny portion needs updating.
Open up the console to see the results. The goal is reduce the number of times "Monstor.Build..." is printed.
#page "/monstor"
#functions {
static int redraw = 0;
static bool doWork = true;
string status = "--";
void HandleEvent(string anEvent) {
status = anEvent; // updates every 2 seconds
StateHasChanged(); // renders MonstrouslyDeepApp--every 2 seconds!
}
async Task work() { // simulate external events
if (doWork) {
doWork = false;
while (true) {
await Task.Delay(2000);
HandleEvent(DateTime.Now.ToLongTimeString());
}
}
}
protected override void OnInit() {
work();
}
void Also_do_monstrous_business_calculations(int x) {
System.Console.WriteLine(" Calculating...");
}
}
#{
Also_do_monstrous_business_calculations(42);
}
<div class="main">
<div class="toolbar row bg-light">
<span style="width:100%;text-align:right"> Status: #status </span>
</div>
<div class="main-app row jumbotron">
<h1>I am Monstor App # #redraw. Fear me!</h1>
#*<MonstrouslyDeepApp M="#ModelRoot" />*#
#{
System.Console.WriteLine($" Monster.Build #{redraw}");
redraw++;
}
</div>
</div>
Here's my solution to my problem. It works but it's kinda convoluted, I hope someone finds a better way...
Basically I wrap the area to be updated-in-isolation with a <Updatable>:
<div class="main">
<Updatable Event="#statusEvent">
<div class="toolbar row bg-light">
<span style="width:100%;text-align:right"> Status: #status </span>
</div>
</Updatable>
:
</div>
You define & use statusEvent like so:
#functions {
:
string status = "--";
UpdateEvent statusEvent = new UpdateEvent();
void HandleEvent(string anEvent) {
status = anEvent; // updates every 2 seconds
//StateHasChanged(); // renders MonstrouslyDeepApp--every second!
statusEvent.StateChanged(); // only renders status area--every second
}
:
}
In Updatable.razor:
#page "/Updatable"
#functions {
[Parameter] UpdateEvent Event { get; set; }
[Parameter] RenderFragment ChildContent { get; set; }
protected override void OnInit() {
Event.OnChange += (s, e) => {
StateHasChanged();
};
}
}
#ChildContent
And finally UpdateEvent is simply an Event wrapper:
public class UpdateEvent {
public event EventHandler OnChange;
public void StateChanged() { OnChange?.Invoke(this, EventArgs.Empty); }
}

Add section comments to my Details view

in my View I have something like this:
<div class="form-group">
<form asp-controller="Ticket" asp-action="Comment" method="post">
<label asp-for="Comment" class="control-label"></label>
<textarea asp-for="Comment.Content" class="form-control" placeholder="Add comment!"></textarea>
<span asp-validation-for="Comment.Content" class="text-danger"></span>
</form>
<input type="submit" value="Add comment" class="btn btn-default" />
This is in my Details View. Now I want to add comment to my model
public class TicketCommentViewModel
{
public Ticket Ticket { get; set; }
public Comment Comment { get; set; }
}
and Controller:
public async Task<IActionResult> Comment(TicketCommentViewModel model)
{
var ticket = await _context.Tickets.FirstOrDefaultAsync(u => u.TicketId == model.Ticket.TicketId);
var user = await GetCurrentUserAsync();
if(ticket == null)
{
return NotFound();
}
model.Comment.SendTime = DateTime.Now;
model.Comment.TicketID = ticket.TicketId;
model.Comment.Ticket = ticket;
model.Comment.UserId = user.Id;
model.Comment.User = user;
_context.Comments.Add(model.Comment);
ticket.Comments = await _context.Comments.ToListAsync();
return View();
}
I have problem with this -> How to go from first code (add Comment) to Controller, and add my Comment to DB.
Can someone help me with that ?
Thanks.
I am not sure what is the problem but I guess that when you click submit your data is not submitted. This is because your submit button is outside the <form>. Try move the submit button inside the <form>
//Two action methods in the controller
public ActionResult AddComment(int PageId, string name, string email, string comment)
{
Comment comment = new Comment()
{
PageID = PageId,
Name = name,
Email = email,
Comment = comment,
CreateDate = DateTime.Now
};
DbContext.Add(jobOffer);
return PartialView("ShowComments", DbContext.Where(c=> c.pageID == PageId));
}
public ActionResult ShowComments(int PageId)
{
return PartialView(DbContext.Where(c=> c.pageID == PageId));
}
//Add the script after the comment div in the View
<script>
function addComment() {
$.ajax({
url: "/Comment/AddComment/"+#Model.PageID,
type: "Get",
data: {
name: $("#txtName").val(), email: $("#txtEmail").val(),
comment : $("#txtComment").val() }
}).done(function(result) {
$("#offerList").html(result);
$("#txtName").val("");
$("#txtEmail").val("");
$("#txtComment").val("");
});
}
</script>

multiple ajax forms in the same mvc 4 view doesn't work properly - after the second time of submit my model is empty

I've got a class Cart with property Lines which returns me a collection of type CartLine. here is my two classes Cart ad CartLine
public class Cart
{
private List<CartLine> lineCollection = new List<CartLine>();
public String Currency { get; set; }
public void UpdateCart(int ArticleID, string selectedQuantityType, int Quantity)
{
CartLine line = lineCollection.Where(p => p.Product.ArticleID == ArticleID).FirstOrDefault();
if (line != null)
{
line.SelectedQuantityType = selectedQuantityType;
line.Quantity = Quantity;
}
}
public IEnumerable<CartLine> Lines
{
get { return lineCollection; }
}
}
<
And here is my class CartLine
public class CartLine
{
public CartLine()
{
this.QuantityType= new List<QuantityType>();
}
public WebServiceBeaMenu Product { get; set; }
public int Quantity { get; set; }
public string Currency { get; set; }
public string SelectedQuantityType { get; set; }
}
I've got two actions which returns me strongly typed PartialView - CartIndexViewModel with two properties - one from Type Cart
public ActionResult CartPartial(string returnUrl)
{
return PartialView(new CartIndexViewModel
{
Cart = GetCart(),
ReturnUrl = returnUrl
});
}
[HttpPost]
public ActionResult CartPartial(CartLine line)
{
Cart crt = GetCart();
crt.UpdateCart(line.Product.ArticleID, line.SelectedQuantityType, line.Quantity);
return PartialView(new CartIndexViewModel
{
Cart = crt
});
}
private Cart GetCart()
{
Cart cart = (Cart)Session["Cart"];
if (cart == null)
{
cart = new Cart();
Session["Cart"] = cart;
}
return cart;
}
ANd here is my view whih loop through all thre cartlines of my cart and display their quantity and quantityType.
The problem is as you see for each CartLine I'm making an ajax form so when there is a change in the quantity of a CartLine (when there is a change in this helper #Html.TextBoxFor(x => line.Quantity,new { id = #liine.Product.ArticleID, onchange="SubmitForm(this.id)" })) you see that I submit the form to the httppost form of my CartPartial Action. The first time when I submit the form there is no problem and thanks to mvc model binding my CartLine parameter is filled with the data I need. The problem is when this action returns me the same view with the updated data after that if I try to change a quantity and submit the form for the second time my CartLine parameter is empty. What may be the reason. Thankls in advance.
#model MvcBeaWeb.Models.CartIndexViewModel
<div id="cartContainer">
<table width="100%" align="center" style="margin:auto;border-collapse:collapse;">
<tbody>
#foreach(var line in Model.Cart.Lines)
{
<tr style="height:90px" >
#using (Ajax.BeginForm("CartPartial", null, new AjaxOptions { UpdateTargetId = "centerCartBody", InsertionMode = InsertionMode.Replace }, new { id = "form" + #line.Product.ArticleID }))
{
<td align="left">#line.Product.SpecialWord</td>
<td >
<div class="inner-content">
#Html.TextBoxFor(x => line.Quantity,new { id = #liine.Product.ArticleID, onchange="SubmitForm(this.id)" })
<span class="qty-type">
#Html.DropDownListFor(x => line.SelectedQuantityType, new SelectList(line.QuantityType, "QuantityID", "Description"))
</span>
</div>
</td>
#Html.HiddenFor(x => line.Product.ArticleID, "Value");
}
</tr>
}
</tbody>
</table>
<div >
#using (Html.BeginForm("Index/","Confirmation"))
{
<input type="submit" id="btnCheckOut" />
}
</div>
</div>
</div>
</div>
script>
function SubmitForm(id)
{
$("#form_" + id).submit();
}
</script>

MVC : System.NullReferenceException model when submit form

I've been trying to pass a model to a partial view with a form. Some of the model fields are already assigned in the GET request. When the form loads I can see the model fields values but after
submiting the form I get this error in this line: #Html.Hidden("From",Model.From):
Object reference not set to an instance of an object
Why these two fields are assigned with null on submit?
My controllers:
[HttpGet]
public ActionResult SendPrivateMessage(string from, List<string> to)
{
// two of the fields are already assigned
return PartialView("SendMessage", new MessageModel(from,to));
}
[HttpPost]
public ActionResult SendPrivateMessage(MessageModel m)
{
string fullname = "";
LoginModel loginData = (LoginModel)(Session["user"]);
if (Session["user"] != null)
{
fullname = loginData.LoginDS.Tables[0].Rows[0][loginData.LoginDS.Tables[0].Columns["fullname"].Ordinal].ToString();
}
m.fullname = fullname;
m.Send();
return PartialView("SendMessage");
}
The partial view:
#model HaifanetMobile.Models.MessageModel
<div id="contact_form">
<a id="back_contact" href="#" style="float:left">
<img style="height:20px; width:30px;" src="~/Images/back_btn.gif" alt="back" />.
</a>
<div id="contactus_title">
<div id="close_contactus" style="float:right"><img style="height:20px; width:20px;" src="~/Images/close_btn.gif" /></div>
</div>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<br />
<fieldset>
#Html.Hidden("From", Model.From) //this is where I get the error
#Html.Hidden("To", Model.To)//this is where I get the error
<div>
#Html.TextBoxFor(m => m.Subject, new { #class = "", placeholder = "subject:", id = "msg_subject", onfocus = "this.placeholder = ''", onblur = "this.placeholder = 'subject:'" })
#Html.ValidationMessageFor(m => m.Subject, "required")
</div>
<div>
#Html.TextAreaFor(m => m.Content, new { #class = "", id = "msg_textarea" })
#Html.ValidationMessageFor(m => m.Content, "required")
</div>
</fieldset>
<p>
<input type="submit" value="send" />
</p>
}
</div>
The Model:
public class MessageModel
{
public string From { get; set; }
public List<string> To { get; set; }
public string Subject {get; set;}
public string Content { get; set; }
public string fullname { get; set; }
public MessageModel(string from, List<string> to)
{
// TODO: Complete member initialization
this.From = from;
this.To = to; ;
}
public MessageModel() {
}
public void Send()
{
ServiceReference2.WebService1Soap ws = new ServiceReference2.WebService1SoapClient();
if (!ws.SendMessage(this.From, this.Content, this.Subject, this.To.ToArray() ,this.fullname))
throw new Exception();
}
}
Thanks in advance
You're forgetting to pass the model to your view.
When you return this view, instead of this:
return PartialView("SendMessage");
you must do this:
return PartialView("SendMessage", m);
Where m is your model. That's why the model is null inside your view.