Blazor: data binding requires additional trigger events - .net-core

UI Section
<div class="input-group mb-3">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupSelect">AppId</label>
</div>
<select class="custom-select" id="inputGroupSelect" #bind="#appId">
#if (appIds != null)
{
foreach (var appId in appIds)
{
<option value="#appId">#appId</option>
}
}
</select>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<label class="input-group-text" for="appKeyFormControlInput">AppKey</label>
</div>
<input type="text" class="form-control" id="appKeyFormControlInput" #bind="#appKey" #bind:event="oninput">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" #onclick="SetAppKey">Get AppKey</button>
</div>
</div>
Code section
#code
{
private IEnumerable<string> appIds;
private string appId { get; set; }
private string appKey { get; set; }
protected override async Task OnInitializedAsync()
{
using HttpClient httpClient = new HttpClient();
var result = await httpClient.GetAsync("API_ADDR");
appIds = JObject.Parse(await result.Content.ReadAsStringAsync())["content"].ToObject<IEnumerable<string>>();
appId = appIds?.FirstOrDefault();
}
private async void SetAppKey()
{
using HttpClient httpClient = new HttpClient();
var result = await (await httpClient.GetAsync("API_ADDR")).Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(result))
{
var app = JObject.Parse(result)["content"].ToObject<Application>();
appKey = app.AppKey;
}
}
}
Expectation
When I click the Get AppKey button,
the value of input#appKeyFromControlInput shows
the appkey of current AppId(which in input#inputGroupSelect).
Actually:
When I click the Get AppKey button,
input#appKeyFromControlInput have no reaction;
When I click the Get AppKey again,
input#appKeyFromControlInput shows the correct appkey.
Environment:
.NET 5 preview 6
Blazor WebAssembly
windows 10 2004

The following code"
<label class="input-group-text" for="appKeyFormControlInput">AppKey</label>
Should be
<label class="input-group-text" for="appKeyFormControlInput">#AppKey</label>
AppKey is interpreted by the compiler as a string, whereas #AppKey is considered an expression that should be evaluated, the result of which is to be displayed between the opening and the closing tags.
You should also changed this: private async void SetAppKey()
to this: private async Task SetAppKey()
When you use async void your method does not return a completed Task object, and thus no one around knows when your component should be re-rendered. Thus, your component is not rendered on the first click, only on the second click, reflecting the values from the first click.
Always use async with Task, not with void.
Note: UI events such as the click event always trigger re-rendering of the component.

Related

How to use RadioButtons and Checkboxes in an asp.net Blazor page

I found it surprisingly difficult to dynamically create and use Radio Buttons and Checkboxes in an asp.net Blazor (Server-side) page, mostly because:
I could not figure out how to bind in a <input type="radio"> field
I came across the EditForm rather late
I did not find (m)any examples
one has to use a loop-local variable to avoid closures (and index out of bound errors)
<InputCheckbox> seems to be unable to bind to a List<bool> and a list of complex objects is required and throws an ArgumentException: The provided expression contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object. error.
So I am posting this question and answering it at the same time with a working solution. The following code first demonstrates the use of regular <input> fields and later uses the EditForm component. As far as I can tell, the latter one is the preferred solution.
I still have one question: How can one use a List<bool> instead of a list of complex objects? It just doesn't feel right having to use a separate class to encapsulate a singe property. It also complicates things if formModel is persisted using EF Core. The same issue was discussed in a similar context e.g. here.
I think your answer over complicates this.
The code below demonstrates a basic setup (it's demo code not production).
It uses the EditForm with a model. There are radio buttons and checkboxes linked into a model that get updated correctly. Selected has a setter so you can put a breakpoint in ans see who's being updated.
#page "/"
#using Blazor.Starter.Data
<EditForm EditContext="this.editContext">
#foreach (var model in models)
{
<h3>#model.Value</h3>
<h5>Check boxes</h5>
foreach (var option in model.Options)
{
<div>
<InputCheckbox #bind-Value="option.Selected" />#option.Value
</div>
}
<h5>Option Select</h5>
<div>
<InputRadioGroup #bind-Value="model.Selected">
#foreach (var option in model.Options)
{
<div>
<InputRadio Value="option.Value" /> #option.Value
</div>
}
</InputRadioGroup>
<div>
Selected: #model.Selected
</div>
</div>
}
</EditForm>
<button class="btn btn-dark" #onclick="OnClick">Check</button>
#code {
private EditContext editContext;
private List<Model> models;
protected override Task OnInitializedAsync()
{
models = Models;
editContext = new EditContext(models);
return Task.CompletedTask;
}
public void OnClick(MouseEventArgs e)
{
var x = true;
}
public List<Model> Models => new List<Model>()
{
new Model() { Value = "Fred"},
new Model() { Value = "Jon"},
};
public class ModelOptions
{
public string Value { get; set; }
public bool Selected
{
get => _Selected;
set
{
_Selected = value;
}
}
public bool _Selected;
}
public class Model
{
public string Value { get; set; }
public string Selected { get; set; }
public List<ModelOptions> Options { get; set; } = new List<ModelOptions>()
{
new ModelOptions() {Value="Tea", Selected=true},
new ModelOptions() {Value="Coffee", Selected=false},
new ModelOptions() {Value="Water", Selected=false},
};
}
}
Here's what it looks like:
Here is the code example mentioned in the question with more detailled comments. First the version without EditForm, which is not the recommended way, but nicely demonstrates the closure problem and why a loop-local variable is required. It also demonstrates my failure to use an <input> field of type="radiobutton" as it seems impossible to bind a value to it:
#page "/"
<h1>Use Checkboxes and Radio Buttons in asp.net Blazor w/o EditForm</h1>
<h2>Checkboxes using a for-loop and a List of bool</h2>
Don't tick the left column, as it generates an index out of bounds error.
#for (int i = 0; i < CheckboxList.Count(); i++)
{
// Gotcha: This is a closure, so the function call is stored together with the environment.
// Since i resides outside of the loop, it is a single variable which is stored with the function call, so
// index out of bound occurs because the last value is used. Instead, use a local variable, so for each
// iteration, a new variable is used and bound to the function. This is a compiler gotcha.
// Since C# 5.0, the loop variable of a foreach lop is inside the loop, so using foreach is save, but it
// does not work with a List<bool> but requires a List of objects than contains a bool as in
// foreach(bool items in CheckboxList), item is the iteration variable and cannot be assigned.
// https://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/
// https://stackoverflow.com/questions/58843339/getting-argumentoutofrangeexception-using-for-loop-in-blazor-component
int ii = i;
<div class="form-check">
<input type="checkbox" #bind=#CheckboxList[i] /><!-- does not work, index out of range -->
<input type="checkbox" #bind=#CheckboxList[ii] />
<label>Answer #ii</label>
</div>
}
Checkbox selection for-loop: #OutTextCheckboxFor
<hr />
<h2>Checkboxes using a foreach-loop and a List<Item></h2>
#foreach (CheckboxItem item in CheckboxItems)
{
<div class="form-check">
<input type="checkbox" #bind=#item.IsChecked />
<label>#item.Title</label>
</div>
}
Checkbox selection foreach-loop: #OutTextCheckboxForEach
<hr />
<h2> Does not work: Radio Buttons using a foreach-loop and a List<Item></h2>
Somehow it seems as if binding a radio button does not work the same way as binding a checkbox. I would expect
that item.Ischecked is true/false, depending on the selection of the radio button.
#foreach (RadioItemBool item in RadioItems)
{
<div class="form-check">
#* would need to add value=, but can't as it is already assigned by bind?? *#
<input type="radio" #bind=#item.IsChecked />
<label>#item.Title</label>
</div>
}
Radio selection foreach-loop: #OutTextRadioForEach
<hr />
<button #onclick="OnSubmit">
Evaluate all the above
</button>
<hr />
#code {
// could also use an array as a direct replacement for the list
// could also be a field instead of a property
public List<bool> CheckboxList { get; set; } = new List<bool> { true, false, true };
public List<CheckboxItem> CheckboxItems = new List<CheckboxItem>() {
new CheckboxItem(true, "Checkbox 1"), new CheckboxItem(false, "Checkbox 2"), new CheckboxItem(true, "Checkbox 3") };
public List<RadioItemBool> RadioItems = new()
{
new RadioItemBool(false, "Radio 1"),
new RadioItemBool(false, "Radio 2"),
new RadioItemBool(false, "Radio 3")
};
string OutTextCheckboxFor;
string OutTextCheckboxForEach;
string OutTextRadioForEach;
public class CheckboxItem
{
public bool IsChecked;
public string Title;
public CheckboxItem(bool isChecked, string title)
{
IsChecked = isChecked;
Title = title;
}
}
public class RadioItemBool
{
public bool IsChecked;
public string Title;
public RadioItemBool(bool isChecked, string title)
{
this.IsChecked = isChecked;
this.Title = title;
}
}
public void OnSubmit()
{
OutTextCheckboxFor = "";
for (int i = 0; i < CheckboxList.Count(); i++)
{
OutTextCheckboxFor += " " + (CheckboxList[i] ? "1" : "0");
}
OutTextCheckboxForEach = "";
foreach (CheckboxItem item in CheckboxItems)
{
OutTextCheckboxForEach += " " + (item.IsChecked ? "1" : "0");
}
OutTextRadioForEach = "";
foreach (RadioItemBool item in RadioItems)
{
OutTextRadioForEach += " " + (item.IsChecked);
}
}
}
And here is the preferred version with EditForm, also demonstrating data validation for a text input. It also shows that it seems impossible to bind the checkboxes to a list of simple types (List<bool>) and a list of a complex type (List<myBool>) is required to prevent the ArgumentException: The provided expression contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object. exception (c.f. here). Often, the text shown in the checkboxes might also be stored in the separate class (myBool), but to demonstrate the shortcoming of not beeing able to bind to a list of bool, only the bool is inside a separte class.
#page "/"
#using System.ComponentModel.DataAnnotations
<h2>EditForm input with validation</h2>
#* https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation *#
<EditForm Model="#formModel" OnValidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group row">
<label for="ShortText" class="col-sm-2 col-form-label">Short Text</label>
<div class="col-sm-6">
<InputText #bind-Value="formModel.ShortText" class="form-control" id="ShortText" aria-describedby="ShortTextHelp"
placeholder="Enter short text" />
<small id="ShortTextHelp" class="form-text text-muted">This is a required field.</small>
</div>
</div>
<div class="form-group row">
<label for="Checkboxes" class="col-sm-2 col-form-label">Checkbox</label>
<div id="Checkboxes" class="col-sm-10">
#for (int i = 0; i < formModel.IsCheckedComplex.Count; i++)
{
// prevent closures (IsCheckedComplex[ii] needs to be a loop-local variable)
int ii = i;
<div class="col-sm-10">
#* Unfortunately, using #bind-value with a List<bool> does not work (see comment below *1) *#
<InputCheckbox class="form-check-input" id="#ii" #bind-Value="#formModel.IsCheckedComplex[ii].IsChecked" />
<label class="form-check-label" for=#ii>"Text for item"</label>
</div>
}
</div>
</div>
<div class="form-group row">
<label for="RadioButtons" class="col-sm-2 col-form-label">Radio buttons</label>
<div id="RadioButtons" class="col-sm-10">
<InputRadioGroup id="RadioButtons" #bind-Value=#formModel.SelectedRadio>
#foreach (var item in formModel.RadioItems)
{
<div class="col-sm-10">
<InputRadio class="form-check-input" id=#item.Index Value=#item.Index />
<label class="form-check-label" for=#item.Index>#item.Title</label>
</div>
}
</InputRadioGroup>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
<p>Inspect formModel in HandleValidSubmit() to see user inputs.</p>
</EditForm>
#code {
private FormModel formModel = new();
public class FormModel
{
public string Text { get; set; }
[Required]
[StringLength(10, ErrorMessage = "Short Text is too long.")]
public string ShortText { get; set; }
// === for the checkboxes
// unfortunately, binding to a List<bool> is not possible; try replacing IsCheckedComplex with IsChecked above *1
public List<bool> IsChecked { get; set; } = new() { true, false, false };
public List<myBool> IsCheckedComplex { get; set; } = new() { new(true), new(false), new(false) };
// a complex object is required as using List<bool> directly, throws an ArgumentException: The provided expression
// contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member
// accessors (fields, properties) of an object. See e.g. https://github.com/dotnet/aspnetcore/issues/12000
public class myBool
{
public bool IsChecked { get; set; }
// store the text shown in the checkbox items here as well
public myBool(bool init)
{
IsChecked = init;
}
}
// === for the radio buttons
public int SelectedRadio = 2;
public class RadioItem
{
public int Index;
public string Title;
public RadioItem(int index, string title)
{
this.Index = index;
this.Title = title;
}
}
public List<RadioItem> RadioItems = new()
{
new RadioItem(1, "Radio 1"),
new RadioItem(2, "Radio 2"),
new RadioItem(3, "Radio 3")
};
}
private void HandleValidSubmit()
{
// HandleValidSubmit called
}
}

How to redirect a ASP.NET Core MVC partial view after file download

I have an asp.net core MVC partial view called ExportPagePartial that allows user to export a page from the system and downloads it. In the HttpGet controller action I show the partial view (as a Modal pop-up) to get user input.
Modal Popup
<a class="dropdown-item" asp-action="ExportPagePartial" asp-route-userId="#Model.UserId" asp-route-businessAccountId="#Model.BusinessAccountId" asp-route-projectId="#Model.ProjectId" asp-route-pageId="#Model.PageId" data-toggle="modal" data-target="#ModalPlaceholder" title="Export page."><i class="fas fa-cloud-download-alt"></i> Export</a>
Controller Get Action
[HttpGet]
public IActionResult ExportPagePartial(string userId, string businessAccountId, string projectId, string pageId)
{
ExportPageViewModel model = new ExportPageViewModel()
{
// Set properties
};
return PartialView(nameof(ExportPagePartial), model);
}
Once the user hits Export button from the Modal pop-up partial view (which is a form submit action) the following HTTPPost action is correctly called.
In this action I have to get the file from the Web Api and then download it via the browser, however after download is complete i want to close the partial view. Once the download is complete the partial view is still visible.
The return action never works and partial modal pop-up view does not close
return RedirectToAction(nameof(BlahRedirectAction));
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ExportPagePartial(ExportPageViewModel model)
{
// Call Web API to get the file
string downloadUrl = "blah_blah_url";
using (HttpResponseMessage httpResponse = await WebApiClient.HttpClient.PostAsJsonAsync(downloadUrl, unprotectedExportInput))
{
if (!httpResponse.IsSuccessStatusCode)
{
throw new InvalidOperationException(await httpResponse.Content.ReadAsStringAsync());
}
// Download the file now.
ActionContext actionContext = new ActionContext(HttpContext, ControllerContext.RouteData, ControllerContext.ActionDescriptor, ModelState);
FileStreamResult fileContent = File(await httpResponse.Content.ReadAsStreamAsync(), httpResponse.Content.Headers.ContentType.MediaType, httpResponse.Content.Headers.ContentDisposition.FileName);
await fileContent.ExecuteResultAsync(actionContext);
}
// Redirect to main pain
// The view never redirects and partial view is still visible
return RedirectToAction(nameof(BlahRedirectAction));
}
fileContent.ExecuteResultAsync(actionContext);
This is because when you download the file, ExportPagePartial has determined the return flow, and will not perform the RedirectToAction.
I suggest that you change the post method that triggers ExportPagePartial to ajax to achieve, so that you can successfully execute ExportPagePartial and after the method, redirect the page to what you want in js.
Here is a complete code of my demo based on your code:
public class ExportTestController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpGet]
public IActionResult ExportPagePartial(string userId, string businessAccountId, string projectId, string pageId)
{
ExportPageViewModel model = new ExportPageViewModel()
{
Id = 1,
Gender = "male",
Name = "aaa",
Number = "1231244"
};
return PartialView(nameof(ExportPagePartial), model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ExportPagePartial(ExportPageViewModel model)
{
// Call Web API to get the file
string downloadUrl = "blah_blah_url";
using (HttpResponseMessage httpResponse = await WebApiClient.HttpClient.PostAsJsonAsync(downloadUrl, unprotectedExportInput))
{
if (!httpResponse.IsSuccessStatusCode)
{
throw new InvalidOperationException(await httpResponse.Content.ReadAsStringAsync());
}
// Download the file now.
ActionContext actionContext = new ActionContext(HttpContext, ControllerContext.RouteData, ControllerContext.ActionDescriptor, ModelState);
FileStreamResult fileContent = File(await httpResponse.Content.ReadAsStreamAsync(), httpResponse.Content.Headers.ContentType.MediaType, httpResponse.Content.Headers.ContentDisposition.FileName);
await fileContent.ExecuteResultAsync(actionContext);
}
// Redirect to main pain
// The view never redirects and partial view is still visible
return RedirectToAction(nameof(BlahRedirectAction));
}
Index.cshtml:
#{
ViewData["Title"] = "Index";
Layout = null;
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script>
$(function () {
$("a").click(function () {
var route = $(this).attr("href");
$('#partial').load(route);
})
$("form").submit(function () {
$.ajax({
url: $("form").attr('action'),
type: 'Post',
data: $("form").serializeArray(),
success: function () {
//$("#ModalPlaceholder").hide();
window.location.href = "/ExportTest/BlahRedirectAction";
}
});
})
})
</script>
<a class="dropdown-item" asp-action="ExportPagePartial"
asp-route-userId="1" asp-route-businessAccountId="1"
asp-route-projectId="1" asp-route-pageId="1"
data-toggle="modal" data-target="#ModalPlaceholder" title="Export page."><i class="fas fa-cloud-download-alt"></i> Export</a>
<div class="modal fade" id="ModalPlaceholder" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<form asp-action="ExportPagePartial" method="post">
<div id="partial">
</div>
</form>
</div>
ExportPagePartial.cshtml:
#model ExportPageViewModel
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label asp-for="Id" class="control-label">#Model.Id</label>
<input asp-for="Id" class="form-control" hidden />
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Gender" class="control-label"></label>
<input asp-for="Gender" class="form-control" />
<span asp-validation-for="Gender" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Number" class="control-label"></label>
<input asp-for="Number" class="form-control" />
<span asp-validation-for="Number" class="text-danger"></span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" >Save changes</button>
</div>
</div>
</div>
Here is the test result:

Asp net core 2.2, updating usename not working

I want my user to be able to change his username so i made this:
<form asp-action="UpdateUserProfile" asp-controller="Account" method="post">
<div class="account-details-item">
<h2 class="center-text text-left account-details-item-title">UserName:</h2>
<input name="username" id="username-text" readonly="readonly" class="center-text account-details-item-value" asp-for="User.UserName" value=#Model.User.UserName>
<a id="btn-username" class="account-details-item-btn" >Edit</a>
</div>
<div class="account-details-item">
<h2 class="center-text text-left account-details-item-title">Email:</h2>
<input name="email" readonly="readonly" id="email-text" class="center-text account-details-item-value email" asp-for="User.Email" value=#Model.User.Email />
<a id="btn-email" class="account-details-item-btn" >Edit</a>
</div>
<div class="account-details-item">
<h2 class="center-text text-left account-details-item-title">Phone number:</h2>
<input name="phonenumber" readonly="readonly" id="phone-text" class="center-text account-details-item-value" asp-for="User.PhoneNumber" value=#Model.User.PhoneNumber>
<a id="btn-phone" class="account-details-item-btn" >Edit</a>
</div>
<div class="btns-container">
<div class="btn-item"><a asp-action="Index" asp-controller="Cart" asp-route-id=#Model.User.CartId>Go To Cart</a></div>
<div id="save-btn" class="btn-item"><button type="submit">Save Changes</button></div>
</div>
</form>
And in AccountController:
[HttpPost]
public IActionResult UpdateUserProfile()
{
var username = Request.Form["username"];
var phonenumber = Request.Form["phonenumber"];
var email = Request.Form["email"];
var user = _userService.GetUser(User);
if(`enter code here`IsUsernameDiffrent(username))
{
_userService.UpdateUsername(User, username);
_userManager.UpdateAsync(user.Result);
}
else if(IsEmailDiffrent(email))
{
_userService.UpdateEmail(User, email);
_userManager.UpdateAsync(user.Result);
}
else if (IsPhoneNumberDiffrent(phonenumber))
{
_userService.UpdatePhoneNumber(User, phonenumber);
_userManager.UpdateAsync(user.Result);
}
return RedirectToAction("Index");
}
And in Service Class:
public async void UpdateUsername(ClaimsPrincipal user, string newUsername)
{
var currentUser = await GetUser(user);
currentUser.UserName = newUsername;
_dbContext.SaveChanges();
}
The issue is that if user change his username he still have to login with the old one,
changes are made in database but whenever i try to login with new username it says "Invalid login attempt"
I have an Update User Action in my project and it does work in mine, and its little different from yours, but you can try to change it like that:
[HttpPost]
public async Task<IActionResult> UpdateUserProfile()
{
var user = _userService.GetUser(User);
if (user == null)
{
return StatusCode(404);
}
var username = Request.Form["username"];
if(IsUsernameDifferent(username))
{
user.UserName = username;
var result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return RedirectToAction("Action", "Controller");
}
return View();
}
return View();
}
I feel that more code must be seen to analyze the issue. But, make sure you are using transaction scope in your services when you are making changes to the data in the database like update or delete.
Using this technique will ensure that the code is consistent. Changes to the database will not happen unless everything inside the scope is successful. For example, if you are updating and then deleting, what if while deleting an error occurs. Then your data will be updated in the database but not deleted. This is to make sure that both happens successfully or nothing happens at all.
Refer this link.
https://learn.microsoft.com/en-us/ef/core/saving/transactions

Multiple submit buttons in Blazor EditForm?

Using AspNet Blazor and its EditForm:
I am creating a simple form that should contain both an update and a delete button. I do not seem to find any examples of how to pass parameters to the submit.
I have tried to place an #onclick in the delete button pointing towards DeleteObject, but then I get no validation (I actually do not need validation in this case, but I want to do it anyway), plus that the SaveObject was also called after the delete...
<EditForm Model="#selectedCar" OnValidSubmit="#SaveObject">
<DataAnnotationsValidator />
<ValidationSummary />
....My <InputText>'s for all values I have in my object
<button type="submit" class="btn btn-primary" value="Save">Spara</button>
<button type="submit" class="btn btn-primary" value="Delete">Delete</button>
</EditForm>
#code {
[Parameter]
public string Id { get; set; }
CarModel selectedCar;
protected override async Task OnInitializedAsync()
{
selectedCar = await _CarService.GetCar(int.Parse(Id));
}
protected async Task SaveObject()
{
selectedCar.Id = await _CarService.SaveCar(selectedCar);
}
protected async Task DeleteObject(int Id)
{
selectedCar.Id = await _CarService.DeleteCar(selectedCar);
}
}
I want to be able to call specific functions for each button, without going around validation.
Anyone have a good idea how to do this?
Ok, I ended up with the following solution. It seems to work as expected.
<EditForm Model="#selectedCar" Context="formContext">
<DataAnnotationsValidator />
<ValidationSummary />
....My <InputText>'s for all values I have in my object
<button type="submit" class="btn btn-primary" #onclick="#(() => SaveCar(formContext))">Save</button>
<button type="submit" class="btn btn-primary" #onclick="#(() => UpdateStockQuantity(formContext))">Update stock quantity</button>
<button type="submit" class="btn btn-secondary" #onclick="#(() => DeleteCar(formContext))">Delete</button>
</EditForm>
#code {
[Parameter]
public string Id { get; set; }
CarModel selectedCar;
protected override async Task OnInitializedAsync()
{
selectedCar = await _CarService.GetCar(int.Parse(Id));
}
protected async Task SaveCar(EditContext formContext)
{
bool formIsValid = formContext.Validate();
if (formIsValid == false)
return;
selectedCar.Id = await _CarService.SaveCar(selectedCar);
}
... plus same approach with UpdateStockQuantity and DeleteCar.
}
The two buttons will submit the form with the validations.
And then you can check on the Boolean and call any logic you want:
<EditForm Model="#Input" OnValidSubmit="#UpdateAsync">
<DataAnnotationsValidator />
<div class="row">
<div class="form-group col-md-12">
<label class="required"> Name</label>
<InputText class="form-control" #bind-Value="Input.Name" />
<span class="err"><ValidationMessage For="#(() => Input.Name)" /></span>
</div>
</div>
<div class="text-center">
<button type="submit" #onclick="#(()=> Input.IsNew = false)" class="btn">save 1</button>
<button type="submit" #onclick="#(()=> Input.IsNew = true)" class="btn">save 2</button>
</div>
</EditForm>
#code{
async Task UpdateAsync()
{
if (Input.IsNew)
{
//do somthing
}
else
{
//do another somthing
}
}
}
If you use type="button" then only the #onclick method is called and not the OnValidSubmit method. But this way there's no validation.

Adding multiple set of key-value pair of data in ASP.Net Core with Razor syntax

I need to create a form that collects multiple key-value pairs of data from the user. I am using a dictionary to model this data. In my form, the user inserts a key and value and if needs to add more than one pair then a (+) icon is pressed for new fields to appear. In case the user decides to delete a key and value pair, a delete icon needs to be clicked to remove the key-value pair. I started coding the behavior of the form with jQuery and JavaScript as the backend is made in C#, but soon it started to get more complicated. I have tried to use Razor page and asp tag helpers, but they are not helpful in creating the sequence of events as I would like to. I am wondering how such a situation can be addressed most appropriately in ASP.Net Core 2.2
<div id="otherProperty">
<i id="deleteProperty" class="material-icons">delete</i>
<input id="otherPropertyKey" name="propertyKey" type="text" placeholder="Property name"/>
<input id="otherPropertyValue" name="propertyValue" type="text" placeholder="Property value"/>
<i id="addProperty" class="material-icons"></i>
</div>
<div>
<input id="submitBtn" type="submit" value="Submit"/>
</div>
The model for the OtherProperties is:
public class OtherProperties:IOtherProperties
{
private readonly Dictionary<string, string> _otherProperties;
public OtherProperties()
{
_otherProperties = new Dictionary<string, string>();
}
public Dictionary<string, string> GetOtherProperties()
{
return _otherProperties;
}
public void AddProperty(string key, string value)
{
if (!_otherProperties.ContainsKey(key))
{
_otherProperties.Add(key,value);
}
}
public void RemoveProperty(string key)
{
if (_otherProperties.ContainsKey(key))
{
_otherProperties.Remove(key);
}
}
}
Where the interface is
public interface IOtherProperties
{
Dictionary<string, string> GetOtherProperties();
void AddProperty(string key, string value);
void RemoveProperty(string key);
}
I finally resolved this by using JavaScript. Without Blazor, you can't use pure ASP.Net Razor to achieve this. The JavaScript code that I wrote is as follows:
<div id="otherProperties">
<div id="otherProperty-0">
<i id="deleteProperty-0" class="material-icons" onclick="RemoveProperty(id)">delete</i>
<input id="otherPropertyKey-0" name="propertyKey-0" type="text" placeholder="Property name"/>
<input id="otherPropertyValue-0" name="propertyValue-0" type="text" placeholder="Property value"/>
<i id="addProperty" class="material-icons" onclick="AddProperty()"></i>
</div>
Then JavaScript code would be like this
var idCounter = 1;
var keyValuePairArray = ['0'];
function AddProperty() {
if(document.getElementById('deleteProperty-'.concat(idCounter-1)) !== null) {
document.getElementById('deleteProperty-'.concat(idCounter - 1)).style.visibility = 'visible';
}
var newProperty = GenerateOtherProperty(idCounter);
document.getElementById('otherProperties').insertAdjacentHTML('beforeend',newProperty);
keyValuePairArray.push(idCounter.toString());
idCounter = idCounter +1 ;
}
function RemoveProperty(id) {
//from id extract the number
var propId = id.toString().split('-')[1];
document.getElementById('otherProperty-'.concat(propId)).remove();
var index = keyValuePairArray.indexOf(propId.toString(),0);
if(index > -1){
keyValuePairArray.splice(index,1);
}
}
function KeyValuePair(key,value) {
this.key = key;
this.value = value;
}
function GetKeyValueInputBoxes() {
var otherProps = [];
for (i =0; i < keyValuePairArray.length; i++){
var key = GetElement('otherPropertyKey-'.concat(keyValuePairArray[i])).value;
var value = GetElement('otherPropertyValue-'.concat(keyValuePairArray[i])).value;
var kvp = new KeyValuePair(key,value);
otherProps.push(kvp);
}
return otherProps;
}
/**
* #return {string}
*/
function GenerateOtherProperty (counter) {
var rawOtherProperty = ' <div id="otherProperty">\n' +
' <i id="deleteProperty" class="material-icons" onclick="RemoveProperty(id)">delete</i>\n' +
' <input id="otherPropertyKey" name="propertyKey" type="text" placeholder="Property name"/>\n' +
' <input id="otherPropertyValue" name="propertyValue" type="text" placeholder="Property value"/>\n' +
' <i id="addProperty" class="material-icons" onclick="AddProperty()"></i>\n' +
' </div>';
var freshProp = new DOMParser().parseFromString(rawOtherProperty,"text/html");
freshProp.getElementById("otherProperty").id = 'otherProperty-'.concat(counter);
freshProp.getElementById("deleteProperty").id = 'deleteProperty-'.concat(counter);
freshProp.getElementById("otherPropertyKey").id = 'otherPropertyKey-'.concat(counter);
freshProp.getElementById("otherPropertyValue").id = 'otherPropertyValue-'.concat(counter);
return freshProp.body.innerHTML;
}
So, by using JS we control the behavior of the UI and collect the values in the fields. We will convert the data to a JSON object and POST it as the raw body of a request to our Web API controller in the back.
[HttpPost]
[Route("api/event")]
public IActionResult ProcessForm([FromBody] JObject jsonRawBody)
{
var jsonData = JsonConvert.DeserializeObject<Event>(jsonRawBody.ToString());
// process jsonData
}
Where Event model is :
public class Event
{
[JsonProperty("otherKVP")]
public IList<KeyValuePair<string,string>> OtherProperties { get; set; }
}

Resources