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.
Related
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:
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); }
}
Is it OK to call a method in the #functions{} section in razor pages directly from HTML? This seems to work fine and it's much easier than calling an API, but I was wondering if there is a downside to this (security, performance, etc)?
For example, in the code...
#functions {
public class Tickets: PageModel
{
public ApplicationDbContext _db { get; set; }
public Tickets(ApplicationDbContext db)
{
_db = db;
}
public void OnGet()
{
}
public string GetTickets(int Top) //--> THIS IS THE METHOD I AM CALLING
{
var data = _db.Tickets.OrderByDescending(x => x.CreatedAt).Take(Top);
var jdata = JsonConvert.SerializeObject(data.ToList());
return jdata;
}
}
}
And the HTML...
<div class="card alert-warning" v-for="ticket in tickets">
<div class="card-body">
<h4 class="card-title">{{ticket.TicketSubject}}</h4>
Card link
Another link
</div>
</div>
#section Scripts {
<script>
new Vue({
el: '#app',
data: {
dismissSecs: 5,
dismissCountDown: 5,
tickets: #Html.Raw(Model.GetTickets(100)), //-->THIS WORKS, BUT IT IS OK TO USE LIKE THIS?
xx: ''
}
})
</script>
}
It works because I believe the method is called when the page is being rendered and you have constant value being passed in. It would not automatically do something like update client-side without AJAX code being involved.
But to answer you question I think the 'more correct' approach is to set a binding property in the OnGet and reference that client-side
public class Tickets : PageModel
{
public ApplicationDbContext _db { get; set; }
public Tickets(ApplicationDbContext db)
{
_db = db;
}
[BindProperty]
public string TicketData { get; set; }
public void OnGet()
{
TicketData = GetTickets(100);
}
public string GetTickets(int Top)
{
var data = _db.Tickets.OrderByDescending(x => x.CreatedAt).Take(Top);
var jdata = JsonConvert.SerializeObject(data.ToList());
return jdata;
}
}
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.
Is there any standard practice to display errors in a view? Currently it is being displayed from TempData.
I implemented a derived a class from Base Controller and used that derived class in every one of my controller. Then assign error or success messages from controller.
public class TestController : Controller
{
public string ErrorMessage
{
get { return (string) TempData[CommonHelper.ErrorMessageKey]; }
set
{
if (TempData.ContainsKey(CommonHelper.ErrorMessageKey))
{
TempData[CommonHelper.ErrorMessageKey] = value;
}
else
{
TempData.Add(CommonHelper.ErrorMessageKey,value);
}
TempData.Remove(CommonHelper.SuccessMessageKey);
}
}
public string SuccessMessage
{
get { return (string)TempData[CommonHelper.SuccessMessageKey]; }
set
{
if(TempData.ContainsKey(CommonHelper.SuccessMessageKey))
{
TempData[CommonHelper.SuccessMessageKey] = value;
}
else
{
TempData.Add(CommonHelper.SuccessMessageKey, value);
}
TempData.Remove(CommonHelper.ErrorMessageKey);
}
}
}
CommonHelper Class
public class CommonHelper
{
public const string SuccessMessageKey = "successMessage";
public const string ErrorMessageKey = "errorMessage";
public static string GetSuccessMessage(object data)
{
return data == null ? string.Empty : (string) data;
}
public static string GetErrorMessage(object data)
{
return data == null ? string.Empty : (string) data;
}
}
Then created a partial view having this
#using Web.Helpers
#if (!string.IsNullOrEmpty(CommonHelper.GetSuccessMessage(TempData[CommonHelper.SuccessMessageKey])))
{
<div class="alert alert-success">
#CommonHelper.GetSuccessMessage(TempData[CommonHelper.SuccessMessageKey])
</div>
}
else if (!string.IsNullOrEmpty(CommonHelper.GetErrorMessage(TempData[CommonHelper.ErrorMessageKey])))
{
<div class="alert alert-success">
#CommonHelper.GetErrorMessage(TempData[CommonHelper.ErrorMessageKey])
</div>
}
And in every view, the partial view is rendered.
<div>
#Html.Partial("_Message")
</div>
Here is a pretty common implementation of displaying errors.
Controller
public class UserController : Controller
{
[HttpPost]
public ActionResult Create(User model)
{
// Example of manual validation
if(model.Username == "Admin")
{
ModelState.AddModelError("AdminError", "Sorry, username can't be admin")
}
if(!ModelState.IsValid()
{
return View(model)
}
}
}
Model
public class User
{
[Required]
public string Username {get; set;}
public string Name {get; set; }
}
View
#Html.ValidationSummary(true)
#using(Html.BeginForm())
{
// Form Html here
}
You don't need all of the infrastructure you created. This is handled by the framework. If you need a way to add success messages you can checkout the Nuget Package MVC FLASH
I prefer to use ModelState.AddModelError()