Blazor how to submit form without submit button - .net-core

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

Related

Using EditForm (Blazor ASP .NET) Validation to ensure duplicate inputs aren't created in Database

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

Blazor Client Update Model Form Pre-population

I'm building a CRUD application in Blazor WebAssembly, and on the Update View I have:
UpdateForecast.razor
#page "/forecast/update/{Id}"
#using System.Text.Json
#using System.Text.Json.Serialization
#using WeatherBA.Shared.Dtos
#using WeatherBA.Shared.Responses
#inject HttpClient Http
#inject NavigationManager NavManager
<PageTitle>Update forecast</PageTitle>
<h1>Update forecast</h1>
#if (forecast != null)
{
<EditForm Model="#forecast" OnValidSubmit="#SaveForecast">
<table>
<tr><label>Id:</label></tr>
<tr><input type="text" #bind-value="forecast.Id"/></tr>
<tr><label>Date:</label></tr>
<tr><input type="text" #bind-value="forecast.Date"/></tr>
<tr><label>Temperature:</label></tr>
<tr><input type="text" #bind-value="forecast.TemperatureC"/></tr>
<tr><label>Summary:</label></tr>
<tr><input type="text" #bind-value="forecast.Summary"/></tr>
<input type="submit" value="Save"/>
</table>
</EditForm>
} else
{
<p>Loadnig...</p>
}
#code {
[Parameter]
public string Id { get; set; }
private ForecastDto? forecast = new ForecastDto();
private CreateForecastCommandResponse? response = null;
protected override async Task OnInitializedAsync()
{
forecast = await Http.GetFromJsonAsync<ForecastDto> ("api/Forecast" + Id);
//not filling form but but api return correct object
}
protected async Task SaveForecast()
{
//response = new CreateForecastCommandResponse();
HttpResponseMessage frame = await Http.PutAsJsonAsync("api/Forecast/" + Id, forecast);
response = await frame.Content.ReadFromJsonAsync<CreateForecastCommandResponse>();
if(response.Success == true)
NavManager.NavigateTo("/forecast/" + Id);
}
}
OnInitializedAsync() has to fetch the data and populate input fields with values of given object.
The problem is it doesn't. Any sugestions why can it be?

Blazor Howto - looking for winform MessageBox() alike function

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

Dynamic Property and Child Model Not Binding

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.

ASP.Net Core MVC save to database method is not fired for the first time.After I go back and fire it again it works perfectly

I have an app in which I post some JSON data to my server and then create an object that will later be added to a database.This is done by the "Save" method in my "SendItemsController":
[Route("SendItems/Save")]
[ApiController]
public class SendItemsController : Controller
{
private AppDbContext _db;
public SendItemsController(AppDbContext db)
{
_db = db;
}
[HttpPost]
[Consumes("application/json")]
public async Task<IActionResult> Save([FromBody] ShoppingCart s)
{
await _db.ShoppingCarts.AddAsync(s);
await _db.SaveChangesAsync();
return RedirectToAction("Index");
}
[HttpGet("~/ThankYou/Index")]
public IActionResult Index()
{
return View();
}
}
After the object is added to the database I try to redirect the client to a "ThankYou" page.
When first launched when I press my order button on the "Cart" page it redirects the client to the "ThankYou" page without firing the "Save" method, but if I go back from the "ThankYou" page to the "Cart" page and hit the button again, the method is fired, the object is added to the database and the client is redirected to the "ThankYou" page, just as it should.My question is what should I change to my code in order to make it fire from the first time, not after going back and hitting the order button again.
I will provide the code that I have written.
Here is the javascript that I use in order to form my JSON object and then POST it to my server:
var orderB = document.getElementById("orderB");
orderB.addEventListener("click", function () {
var inputName = document.getElementById("inputName").value;
var inputAddress = document.getElementById("inputAddress").value;
var inputMail = document.getElementById("inputMail").value;
var auxArray = [];
for (var i = 0; i < productsAux.length; i++) {
if (productsAux[i]!="") {
auxArray[i-1] = { "productName": productsAux[i].titlu, "productPrice": productsAux[i].pret, "quantity": localStorage.getItem(productsAux[i].titlu) };
}
}
var shoppingCart = {
productList: auxArray,
clientName: inputName,
clientAddress: inputAddress,
clientMail: inputMail
};
$.ajax({
type: "POST",
data: JSON.stringify(shoppingCart),
url: "senditems/save",
contentType: "application/json;charset=utf-8",
})
})
And here is the html for my form that I use to gather the name,address and email from the client :
<div class="form-group row">
<div class="col-4">
<label id="clientName"></label>
</div>
<div class="col-6">
<input id="inputName" type="text" />
</div>
</div>
<div class="form-group row">
<div class="col-4">
<label id="clientAddress"></label>
</div>
<div class="col-6">
<input id="inputAddress" type="text" />
</div>
</div>
<div class="form-group row">
<div class="col-4">
<label id="clientMail"></label>
</div>
<div class="col-6">
<input id="inputMail" type="text" />
</div>
</div>
<div class="form-group row">
<div class="col-3 offset-4">
<button class="btn btn-primary " id="orderB" asp-controller="SendItems" action="Save">ORDER</button>
</div>
</div>
</form>
Firstly,I am not sure if you have provided the whole ajax,because an ajax requested action will not let you redirect in the backend.You could only redirect it in your ajax function.
Secondly,you have added the onclick event,no need to add asp-controller tag helper it would render wrong formaction.
Thirdly,the item i starts with 0 but the index of auxArray starts with -1,the product array may not pass to the backend successfully.
At last,the ajax url should be:/senditems/save.
Here is a working demo like below:
Model:
public class ShoppingCart
{
public string clientName { get; set; }
public string clientAddress { get; set; }
public string clientMail { get; set; }
public List<Product> productList { get; set; }
}
public class Product
{
public string productName { get; set; }
public int productPrice { get; set; }
public int quantity { get; set; }
}
View:
<form>
//....
<div class="form-group row">
<div class="col-3 offset-4">
<button class="btn btn-primary" id="orderB" type="button">ORDER</button>
</div>
</div>
</form>
#section Scripts
{
<script>
var orderB = document.getElementById("orderB");
orderB.addEventListener("click", function () {
var inputName = document.getElementById("inputName").value;
var inputAddress = document.getElementById("inputAddress").value;
var inputMail = document.getElementById("inputMail").value;
var auxArray = [];
for (var i = 0; i < 1; i++) {
auxArray[i] = { "productName": "aaaa", "productPrice": 34, "quantity": 3 };
}
var shoppingCart = {
productList: auxArray,
clientName: inputName,
clientAddress: inputAddress,
clientMail: inputMail
};
$.ajax({
type: "POST",
data: JSON.stringify(shoppingCart),
url: "/senditems/save", //add `/`
contentType: "application/json;charset=utf-8",
success: function (res) {
window.location.href = res; //redirect like this
}
})
})
</script>
}
Controller:
[Route("SendItems/Save")]
[ApiController]
public class SendItemsController : Controller
{
[HttpPost]
[Consumes("application/json")]
public async Task<IActionResult> Save([FromBody] ShoppingCart s)
{
return Json("YourRedirectUrl");
}
}
Result:

Resources