make a small change to a large Blazor App - asp.net-core

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); }
}

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.

Creating Blazor component similar to Response.Write

I'm trying to create a component similar to Response.Write("") back in the day for helping me diagnose Blazor lifecycle issues. This is what I have so far, but nothing is printing to the UI. However, when I remove the component and directly write to the UI, I get no problems.
<div>
#foreach (string line in Lines)
{
<div>#line</div>
}
</div>
#code {
[Parameter]
public List<string> Lines
{
get
{
return _lines;
}
set
{
_lines = value;
}
}
In my page component I have an instance of the Debug component like:
<Debug #ref="LocalDebug" />
#code {
protected Debug LocalDebug { get; set }
protected void Function() {
Debug.Lines.Add("Print something");
}
}
You doing it in a wrong way, you can achieve functionality like this
This is your Debug.razor component
<div>
#foreach (string line in Lines)
{
<div>#line</div>
}
</div>
#code {
private List<string> Lines = new();
public void AddLine(string line){
Lines.Add(line);
StateHasChanged();
}
}
This is how you can use it in razor page
<Debug #ref="LocalDebug" />
This is code behind in razor page
#code {
private Debug LocalDebug;
protected void Function() {
LocalDebug.AddLine("Add something");
}
}

Make a form submitting both GET and POST

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:

razor pages core local methods

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;
}
}

Displaying Error in a 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()