I m using VS 2019 and Blazor Server Net5. The included bootstrap 4 environment offers modal dialogs and I would like to replicate something like the MessageBox() from good old Windows Forms. This means you execute (withhin a button click event) something like
DialogResult x = [await] MessageBox[Async]("SomeMsg", SomeButtons+SomeIcon);
And in x you would find what option the user did click (DialogResult.OK, Cancel, ...).
So I found several samples how to show the model dialog itself, and write it even as a component.
I have a simple component MessageBox.razor
public enum ModalResultType { Closed = 0, OK = 1, Cancel = 2, }
#if (ShowMessageBox == true)
{
<div class="modal fade show d-block" id="MessageBox" tabindex="-1"
role="dialog" aria-hidden="true">
.... and so forth ....
<button type="button" class="close" data-dismiss="modal" aria-label="Close"
#onclick="() => OnButtonClick(ModalResultType.Closed)">X</button>
... and so forth ...
<button type="button" class="btn btn-primary" data-dismiss="modal"
#onclick="() => OnButtonClick(ModalResultType.OK)">OK</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"
#onclick="() => OnButtonClick(ModalResultType.Cancel)">Cancel</button>
.. and so forth ...
</div>
}
and in the cs behind file I can turn on the component show flag and display the component.
public async Task<ModalResultType> ShowAsync(string title, string messagetext)
{
Title = title;
Message = messagetext;
ShowMessageBox = true;
StateHasChanged();
//
// Now I m at a loss... how to await here what the User did click???
//
return whatTheUserDidClick;
}
// Click event from button, called with the appropiate ModalResultType
//
public async Task OnButtonClicked(ModalResultType value)
{
ShowMessageBox = false;
//
// Now I am at a loss - how to pass the clicked value into the waiting context
// of the UI above from and "complete" the awaiting ShowAsync();
//
}
The overall idea is to put into the framing App.Razor-Component this MessageBox component and so every "page" or other component has (via cascading parameter) access to the message box. And if it would need to create a modal MessageBox dialog, for example from some button click event, it could simply do so by calling
[CascadingParameter] public MessageBoxComponent AppRazorMessageComonent {get;set;}
public async Task SomeClickEvent()
{
// get some form data
// process them
// question arises ask user if to proceed or defer
if (await AppRazorMessageComponent.ShowAsync("Attention", "Shall we proceed?") == ModalResultType.OK)
{
// do stuff
}
else
{
// do other stuff
}
}
I found sample of modal dialogs where the event handler then is bound right to action to be carried out - like deleting a record. But this is not what I want - I would need to specifically bind the html always to the specifics of the page or component I m in at the time. Or I would need to supply a callback function, which would break my current track; like to set the ShowMessageBox flag, return from the click event, and then proceed along with the logic in another method.
So the question is: how can I await for an event withhin an event handler, which is triggered by an other UI event?
Do I need threads for this - I dont think so. It should be possible by Task, async and await only. But how to create an awaitable object, "signal" the completion, or cancellation, of such a Task? And in a way that it works withhin the Blazor UI component environment.
I made use of the System.Threading.SemaphoreSlim class to acheive the awaitable result in ValueTask<ModalResult<T>> OpenModal() in Modal.cs
BlazorRepl
ModalLauncher.razor
<CascadingValue Value="this">
#if (ModalContent is not null)
{
#ModalContent
}
#ChildContent
</CascadingValue>
ModalLauncher.razor.cs
using Microsoft.AspNetCore.Components;
public partial class ModalLauncher : ComponentBase
{
[Parameter]
public RenderFragment ChildContent { get; set; }
public RenderFragment ModalContent { get; set; }
public void ShowModal(RenderFragment renderFragment)
{
ModalContent = renderFragment;
StateHasChanged();
}
public void CloseModal()
{
ModalContent = null;
StateHasChanged();
}
}
Wrap this around your Layout.
#inherits LayoutComponentBase
<ModalLauncher>
<div class="page">
...
</div>
</ModalLauncher>
Modal.cs
public class Modal<T> : ComponentBase
{
[Parameter]
public RenderFragment<ModalContext<T>> ChildContent { get; set; }
[Parameter]
public T Value { get; set; }
[CascadingParameter]
public ModalLauncher Launcher { get; set; }
public async ValueTask<ModalResult<T>> OpenModal(T value)
{
var modalContext = new ModalContext<T> { Modal = this, Value = value };
RenderFragment renderFragment = ChildContent.Invoke(modalContext);
Launcher.ShowModal(renderFragment);
await semaphore.WaitAsync();
return new ModalResult<T> { ModalAction = modalAction, Value = value };
}
public void CancelModal() => CloseModal(ModalAction.Cancel);
public void CloseModal() => CloseModal(ModalAction.Close);
public void OkModal() => CloseModal(ModalAction.Ok);
private void CloseModal(ModalAction action)
{
modalAction = action;
Launcher.CloseModal();
semaphore.Release();
}
private ModalAction modalAction;
private SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);
}
public enum ModalAction
{
Cancel,
Close,
Ok,
}
public class ModalContext<T>
{
public T Value { get; set; }
public Modal<T> Modal { get; set; }
}
public class ModalResult<T>
{
public T Value { get; set; }
public ModalAction ModalAction { get; set; }
}
public class SomeClass
{
public int SomeValue { get; set; }
}
Usage
Note: When I define the modals I only use a type, They are not bound to an instance. When you call OpenModal(...) you can pass an instance then.
#page "/"
<button #onclick="#OpenSomeClassModal">Run Demo</button>
<Modal #ref="someClassModal" T="SomeClass">
...
<input type="number" #bind-value="#context.Value.SomeValue" />
...
<button type="button" class="btn btn-secondary" #onclick="#context.Modal.CancelModal">Cancel</button>
<button type="button" class="btn btn-primary" #onclick="#context.Modal.OkModal">Save changes</button>
...
</Modal>
<Modal #ref="someStringModal" T="string">
...
<p> #context.Value</p>
...
<button type="button" class="btn btn-secondary" #onclick="#context.Modal.OkModal">Close</button>
...
</Modal>
#code {
Modal<SomeClass> someClassModal;
Modal<string> someStringModal;
async Task OpenSomeClassModal()
{
var someClass = new SomeClass { SomeValue = 9 };
var result1 = await someClassModal.OpenModal(someClass);
var result2 = await someStringModal.OpenModal($"The value was set to {result1.Value.SomeValue}, you pressed {result1.ModalAction}");
}
}
You also need to override part of the bootstrap .modal class. Put this in wwwroot\css\app.css:
.modal {
display: block;
}
Related
Need a little help creating custom select component
I am attempting to create a custom form select component. The component will contain my own custom markup rather than using the tag as it needs a completely different UI beyond which I can style with css.
The component should be able to bind it's value to a string / int / decimal model property which is where I am having trouble.
This is what I have so far:
MySelect.razor
#typeparam TValue
#inherits InputBase<TValue>
#namespace Accounting.Web.Components
#foreach (var option in Options)
{
<button #onclick="OnClick(option.Value)">#option.Value</button>
}
MySelect.razor.cs
namespace Accounting.Web.Components
{
public partial class MySelectOption<TValue>
{
public int Id { get; set; }
public TValue Value { get; set; }
}
public partial class MySelect<TValue> : InputBase<TValue>
{
[Parameter]
public string Id { get; set; } = "ESelect";
[Parameter]
public List<MySelectOption<TValue>> Options { get; set; }
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
throw new NotImplementedException();
}
public void OnClick(TValue value)
{
Value = value;
}
}
}
And then in Index.razor:
<MySelect Options="#options" #bind-Value="AddDto.Description" TValue="string">
So when using the component I should be able to bind it to a property of any type (but usually int or string) which I pass as the type param TValue.
However, the line below is causing an issue:
<button #onclick="OnClick(option.Value)">#option.Value</button>
Argument 2: cannot convert from 'void' to 'Microsoft.AspNetCore.Components.EventCallback'
How can I pass the option.Value (which is always a string) to the onCLick event? Or alternatively modify the code above so that I can accomplish my initially stated goal?
You have more that one issue, but the important one is trying to update Value. Value is an "input" into the control. The updated value is passed back to parent by calling ValueChanged. However, calling ValueChanged directly bypasses the built in functionality in InputBase and it's interaction with the EditContext.
This demonstrates the basics of inheriting from InputBase.
To leverage the built in functionality, you need to either:
Set the value by setting CurrentValueAsString from the markup and then providing a custom TryParseValueFromString to convert from a string to your type (there's a BindConverter helper you can use - it's what InputNumber and other input controls use).
Set the value directly by setting CurrentValue. This bypasses TryParseValueFromString.
Your MySelect.
I've prettied up your buttons and abstracted your list to an IEnumerable.
#typeparam TValue
#inherits InputBase<TValue>
#using Microsoft.AspNetCore.Components.Forms;
#using Microsoft.AspNetCore.Components;
#using System.Diagnostics.CodeAnalysis;
<div class="btn-group" role="group">
#foreach (var option in Options)
{
<button class="#btnColour(option.Value)" #onclick="() => OnClick(option.Value)">#option.Value</button>
}
</div>
#code {
[Parameter] public IEnumerable<MySelectOption<TValue>> Options { get; set; } = new List<MySelectOption<TValue>>();
private string btnColour(TValue? value)
{
if (this.Value is null)
return "btn btn-outline-primary";
return this.Value.Equals(value)
? "btn btn-primary"
: "btn btn-outline-primary";
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
throw new NotImplementedException();
}
public void OnClick(TValue? value)
{
CurrentValue = value;
}
}
And then here's a demo page to show it in use.
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<EditForm Model=#model>
<MySelect Options="#options" #bind-Value=model.Description TValue="string" />
</EditForm>
<div class="alert alert-info m-3">
Description: #model.Description
</div>
#code {
private Model model = new();
IEnumerable<MySelectOption<string>> options =
new List<MySelectOption<string>>() {
new MySelectOption<string> { Id = 1, Value = "France" },
new MySelectOption<string> { Id = 1, Value = "Spain" },
new MySelectOption<string> { Id = 1, Value = "Portugal" },
};
public class Model
{
public string? Description { get; set; }
}
}
For reference you can find the source code for all the standard InputBase controls here: https://github.com/dotnet/aspnetcore/tree/main/src/Components/Web/src/Forms
With help and suggestions from previous answers, below is the solution I arrived at:
Index.razor
<MySelect Options="#options" #bind-Value="AddDto.InvestmentEntityId">
</MySelect>
#AddDto.InvestmentEntityId // integer property
<MySelect Options="#options" #bind-Value="AddDto.Description">
</MySelect>
#AddDto.Description // string property
MySelect.razor
#typeparam TValue
#inherits InputBase<TValue>
#namespace Accounting.Web.Components
#foreach (var option in Options)
{
<button #onclick="() => OnClick(option.Value)">#option.Value</button>
}
MySelect.razor.cs
namespace Accounting.Web.Components
{
public partial class MySelectOption
{
public int Id { get; set; }
public string Value { get; set; }
}
public partial class MySelect<TValue> : InputBase<TValue>
{
[Parameter]
public List<MySelectOption> Options { get; set; }
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
if (BindConverter.TryConvertTo<TValue>(value, null, out result))
{
validationErrorMessage = null;
}
else
{
validationErrorMessage = "Err : Select value";
}
}
public void OnClick(string value)
{
TValue tmpValue;
BindConverter.TryConvertTo<TValue>(value, null, out tmpValue);
CurrentValue = tmpValue;
}
}
}
It's probably not perfect but I hope it helps anyone looking to do the same.
I'm creating an application in which a Project is created. The project name I have set out as an EditForm, with the name having a [Required] and a [StringLength] attribute. I'm trying to use the EditForm to run validation, and check that the name of the project doesn't match with one that already exists in the database.
Is this possible to do using EditForm? Or do I need to write a JQuery or something else?
Thanks.
Yes, but you can't easily use a custom attribute because you can't supply the list of existing projects to the attribute at runtime.
You can either:
Use Blazor Fluent Validation (search for "Blazor Fluent Validation")
Build your own validation system (what I do)
Build the validation into a component.
Here's how to build it into a component.
First our data service to get the projects from the database:
namespace SO73539843.Data;
public class ProjectService
{
public IEnumerable<String> Projects { get; private set; } = Enumerable.Empty<string>();
public async ValueTask GetProjects()
{
// emulate a async Db get
await Task.Delay(100);
Projects = new List<string> { "UK", "France", "Portugal" };
}
}
Registered as follows:
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddSingleton<ProjectService>();
Now the component which inherits from InputText. We just override TryParseValueFromString and do our constraint checking there.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace SO73539843.Pages
{
public class InputProject : InputText
{
[Parameter] public IEnumerable<string> Projects { get; set; } = Enumerable.Empty<string>();
protected override bool TryParseValueFromString(string? value, out string? result, [NotNullWhen(false)] out string? validationErrorMessage)
{
validationErrorMessage = null;
if (Projects.Any(item => item.Equals(value, StringComparison.CurrentCultureIgnoreCase)))
validationErrorMessage = $"The {value} project already exists";
result = value;
return validationErrorMessage == null;
}
}
}
And our test page:
#page "/"
#inject ProjectService projectService
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<EditForm Model=this.model OnInvalidSubmit=InValid OnValidSubmit=Valid >
<InputProject #bind-Value=this.model.Value Projects=this.projectService.Projects />
<button type="submit">Add</button>
<ValidationSummary />
</EditForm>
<div class="m-2 p-2 bg-dark text-white">
Value : #this.model.Value
</div>
<div class="m-2 p-2 bg-primary text-white">
Submit : #this.Validation
</div>
#code {
private ModelData model = new ModelData();
private string Validation = "None";
protected async override Task OnInitializedAsync()
=> await this.projectService.GetProjects();
private void Valid()
=> Validation = "Valid";
private void InValid()
=> Validation = "Invalid";
public class ModelData
{
public string? Value { get; set; }
}
}
In a blazor project I used Editform and Fluentvalidation as well as Toolbelt.Blazor.HotKeys for a shortcut (ctrl+s) to submit the form
When I press ctrl+s, the Submit() method is called, but if the form has an error, it does not show the errors. In fact, only the method is called, not the submit form.
What solution do you suggest for this problem?
<EditForm Model="#model" OnValidSubmit="Submit">
<FluentValidationValidator />
...
<button type="submit" >save</button>
</EditForm>
#code
{
[Parameter] public CategoryInfo model { get; set; } = new();
private async Task Submit()
{
var validator = new CategoryValidator();
var result = validator.Validate(model);
if (result.IsValid)
{
...
}
}
}
Here's a working single page component that demos the code needed to implement a form submit on <CTL>S. I've used the DataAnnotationsValidator for simplicity. There are inline comments to explain the methods.
#page "/"
#implements IDisposable
#using Toolbelt.Blazor.HotKeys
#using System.ComponentModel.DataAnnotations;
<h3>EditForm</h3>
<EditForm EditContext="this._editContext" OnValidSubmit="ValidSubmitForm" OnInvalidSubmit="InvalidSubmitForm">
<DataAnnotationsValidator />
<div class="p-2">
<span>Value (100-200):</span>
<InputNumber #bind-Value="_model.Value" />
<ValidationMessage For="() => _model.Value"/>
</div>
<div class="m-2 p-2">
<button class="btn btn-success" type="submit">Submit</button>
</div>
</EditForm>
<div class="m-2 p-2">
<span>#message</span>
</div>
<div class="m-2 p-2">
<button class="btn btn-danger" type="button" #onclick="SubmitFormExternally">Submit Form Externally</button>
</div>
#code {
private string message;
private Model _model = new Model();
[Inject] private HotKeys hotKeys { get; set; }
private HotKeysContext _hotKeysContext;
EditContext _editContext;
// Explicitly setup the Edit context so we have a reference to it
protected override void OnInitialized()
{
_editContext = new EditContext(_model);
_hotKeysContext = this.hotKeys.CreateContext()
.Add(ModKeys.Ctrl, Keys.S, SubmitFormCtlS, "Submit form");
}
// Invalid handler
private Task ValidSubmitForm()
{
message = $"Valid Form Submitted at :{DateTime.Now.ToLongTimeString()}";
return Task.CompletedTask;
}
// Valid Handler
private Task InvalidSubmitForm()
{
message = $" Invalid Form Submitted at :{DateTime.Now.ToLongTimeString()}";
return Task.CompletedTask;
}
// Method to call from external button
// Works and component updates as it's a Blazor Component event
// emulates the private HandleSubmitAsync method in EditForm
private async Task SubmitFormExternally()
{
if (_editContext.Validate())
await this.ValidSubmitForm();
else
await this.InvalidSubmitForm();
}
// Method to call from shortcut key
// The shortcut key mechanism does't wrap the call in a Blazor Component event
// So we wrap the code within one
// The code emulates the private HandleSubmitAsync method in EditForm
private async Task SubmitFormCtlS()
{
var task = Task.CompletedTask;
if (_editContext.Validate())
task = this.ValidSubmitForm();
else
task = this.InvalidSubmitForm();
this.StateHasChanged();
if (!task.IsCompleted || task.IsCanceled)
{
await task;
this.StateHasChanged();
}
}
public void Dispose()
{
_hotKeysContext.Dispose();
}
// Quick Model Class
public class Model
{
[Range(100, 200, ErrorMessage = "Must be between 100 and 200")]
public int Value { get; set; } = 0;
}
}
I want to build dynamic form using Blazor.
Here is my sample component.
#page "/customform"
#using System.Dynamic
#using System.Text.Json
#inject IJSRuntime JSRuntime;
<div class="card m-3">
<h4 class="card-header">Blazor WebAssembly Form Validation Example</h4>
<div class="card-body">
<EditForm EditContext="#editContext"
OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator></DataAnnotationsValidator>
#foreach (var field in Model.Fields)
{
<div class="form-group">
<label>#field.Name</label>
<input #bind-value="field.Value" class="form-control" />
<ValidationMessage For="(()=> field.Value)" />
<ValidationMessage For="(()=> field.Name)" />
<ValidationMessage For="(()=> field)" />
</div>
}
<div class="form-group">
<label>Address</label>
<input #bind-value="Model.Address" class="form-control" />
<ValidationMessage For="()=> Model.Address" />
</div>
<div class="form-group">
<label>Child</label>
<input #bind-value="Model.ChildModel.ChildName" class="form-control" />
<ValidationMessage For="()=> Model.ChildModel.ChildName" />
</div>
<div class="text-left">
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</EditForm>
</div>
</div>
#code{
private SampleModel Model = new SampleModel();
private EditContext editContext;
private ValidationMessageStore _messageStore;
protected override void OnInitialized()
{
editContext = new EditContext(Model);
editContext.OnValidationRequested += ValidationRequested;
_messageStore = new ValidationMessageStore(editContext);
}
private void HandleValidSubmit(EditContext context)
{
var modelJson = JsonSerializer.Serialize(context.Model, new JsonSerializerOptions { WriteIndented = true });
JSRuntime.InvokeVoidAsync("alert", $"SUCCESS!! :-)\n\n{modelJson}");
}
async void ValidationRequested(object sender, ValidationRequestedEventArgs args)
{
_messageStore.Add(editContext.Field("FirstName"), "Test");
_messageStore.Add(editContext.Field("Address"), "Invalid Address");
_messageStore.Add(editContext.Field("ChildModel.ChildName"), "Invalid Child Name");
editContext.NotifyValidationStateChanged();
}
public class SampleModel
{
public string Address { get; set; }
public ChildModel ChildModel { get; set; }
public List<Field> Fields { get; set; }
public SampleModel()
{
this.ChildModel = new ChildModel();
this.Fields = new List<Field>();
this.Fields.Add(new Field()
{
Name = "FirstName",
Value = "",
ControlType = ControlType.Input
});
this.Fields.Add(new Field()
{
Name = "LastName",
Value = "",
ControlType = ControlType.Input
});
}
}
public class ChildModel
{
public string ChildName { get; set; }
}
public enum ControlType
{
Input
}
public class Field
{
public string Value { get; set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public ControlType ControlType { get; set; }
}
}
Currently I am facing too many issues.
If I use For lookup instead of For each it is not working
ChildModel seems to be bind but its validation is not working
Dynamically generated based on Fields collection control does not display validation.
Only address in SimpleModel display validation.
Is there any suggestion or help around this ?
Your profile suggests you know what you're doing, so I'll keep this succinct.
Your for loop needs to look something like this. Set a local "index" variable within the loop to link the controls to. If you don't they point to the last value of i - in this case 2 which is out of range! The razor code is converted to a cs file by the razor builder. You can see the c# file generated in the obj folder structure - obj\Debug\net5.0\Razor\Pages. Note, the linkage of the Validation Message
#for(var i = 0; i < Model.Fields.Count; i++)
{
var index = i;
<div class="form-group">
<label>#Model.Fields[index].Name</label>
<input #bind-value="Model.Fields[index].Value" class="form-control" />
<ValidationMessage For="(()=> Model.Fields[index].Value)" />
</div>
}
Now the message validation store. Here's my rewritten ValidationRequested. Note I'm creating a FieldIdentifier which is the correct way to do it. "Address" works because it's a property of EditContext.Model. If a ValidationMessage doesn't display the message you anticipate, then it's either not being generated, or it's FieldIdentifier doesn't match the field the ValidationMessage is For. This should get you going in whatever project you're involved in - if not add a comment for clarification :-).
void ValidationRequested(object sender, ValidationRequestedEventArgs args)
{
_messageStore.Clear();
_messageStore.Add(new FieldIdentifier(Model.Fields[0], "Value"), "FirstName Validation Message");
_messageStore.Add(new FieldIdentifier(Model.Fields[1], "Value"), "Surname Validation Message");
_messageStore.Add(editContext.Field("FirstName"), "Test");
_messageStore.Add(editContext.Field("Address"), "Invalid Address");
_messageStore.Add(editContext.Field("ChildModel.ChildName"), "Invalid Child Name");
editContext.NotifyValidationStateChanged();
}
If you interested in Validation and want something more that the basic out-of-the-box validation, there's a couple of my articles that might give you info Validation Form State Control or there's a version of Fluent Validation by Chris Sainty out there if you search.
I started with the default app microsoft provides. I took the counter and made a decrease and increase count on the same page. Increase and decrease are child components that are placed inside Counter which is the parent component that just displays the current count. What I was is for the currentCount value to be updated when you hit one of the buttons which are the 2 child components I just mentioned.
I tried looking into state and it seems like way to much work for what I am doing and remember I have no back end.
Here is Counter component or parent comp.
#page "/counter/{currentCount:int}"
<h3>Counter</h3>
<p>Current count: #currentCount</p>
<Decrease currentCount = "#currentCount"></Decrease>
<Increase currentCount = "#currentCount" ></Increase>
#code {
[Parameter]
public int currentCount {get;set;}
}
Decrease count child comp.
<button class="btn btn-secondary" #onclick="decreaseCount">Decrease</button>
#code {
[Parameter]
public int currentCount {get;set;}
public void decreaseCount() {
currentCount--;
}
}
And increase child comp
<button class="btn btn-primary" #onclick="IncrementCount">Increase</button>
#code {
[Parameter]
public int currentCount {get;set;}
private void IncrementCount() {
currentCount++;
}
}
You're very close!
You are initializing the parameters in each control, but the parameters are not bound. I'm not sure if Blazor has introduced a shorthand version for event wiring yet, but the following should work. Note that there's a VERY high chance that Visual Studio will complain that it cannot convert among callback types-- if so, ignore it.
Note also that the EventCallback's name must be EXACTLY the parameter's name with the word "Changed" added to it for it to work:
Main control:
<h3>Counter</h3>
<p>Current count: #currentCount</p>
<Decrease #bind-currentCount="#currentCount"></Decrease>
<Increase #bind-currentCount="#currentCount"></Increase>
#code {
public int currentCount { get; set; }
}
Decrease.razor
<button class="btn btn-secondary" #onclick="decreaseCount">Decrease</button>
#code {
[Parameter]
public int currentCount { get; set; }
[Parameter]
public EventCallback<int> currentCountChanged { get; set; }
private async Task decreaseCount()
{
currentCount--;
await currentCountChanged.InvokeAsync(currentCount);
}
}
Increase.razor
<button class="btn btn-primary" #onclick="IncrementCount">Increase</button>
#code {
[Parameter]
public int currentCount { get; set; }
[Parameter]
public EventCallback<int> currentCountChanged { get; set; }
private async Task IncrementCount()
{
currentCount++;
await currentCountChanged.InvokeAsync(currentCount);
}
}
I don't know who Peter Morris is or why he made the following site for free, but it's where I first learned about event wiring in a way that made sense to me.
https://blazor-university.com/components/two-way-binding/