Getting the ChangeEventArgs value from Blazor component EventCallback? Currently is null after refactor - asp.net-core

I have refactored some Blazor code I wrote to move a dropdown list with onchange function from a page, into a separate Blazor component with an EventHandler Param. Previously it worked inside of the page razor file with onchange function, but now the ChangeEventArgs is null when it is invoked as EventCallback.
Guessing I need to pass the parameter in the #onchange inside the component, if so what syntax to get the currently selected item?
Thanks in advance,
Rob
Page with component tag:
<div class="form-group">
<LocationsDropdown Locations="Locations" Location_OnChange="#Location_OnChange"/>
</div>
Page base class method:
public async Task Location_OnChange(ChangeEventArgs e)
{
PageId = 1;
if (e != null)
LocationId = Convert.ToInt32(e.Value);
await Load().ConfigureAwait(false);
}
New component:
<select class="form-control" #onchange="#(() => Location_OnChange.InvokeAsync())">
<option value="-1">All Locations</option>
#{
if (Locations != null)
{
foreach (var location in Locations)
{
<option value="#location.Id">#location.Description</option>
}
}
}
#code {
[Parameter]
public EventCallback<ChangeEventArgs> Location_OnChange { get; set; }
[Parameter]
public IList<Location>? Locations { get; set; }
}

Just add the argument from the select component onchange event and pass on ...
<select class="form-control" #onchange="#((e) => Location_OnChange.InvokeAsync(e))">

Related

Blazor: Binding values to select/dropdown when using component tag helper in a Razor page

I'm building an app with Blazor Webassembly and I'm doing all the user management related bits of the system server side. I'm using the component tag helper to enable me to use Razor components on my server side Razor pages, but I'm struggling to get my multiselect dropdown list working.
I've created a custom component (Dropdown.razor) that makes use of Radzen's dropdown component which looks like this:
#typeparam TItem
<RadzenDropDown AllowClear="true" AllowFiltering="true" FilterCaseSensitivity="Radzen.FilterCaseSensitivity.CaseInsensitive"
Multiple="true" Placeholder="Please Select" #bind-Value="#BindValue"
Data="#Data" TextProperty="#TextProperty" ValueProperty="Id" />
#code {
[Parameter] public IEnumerable<int> BindValue { get; set; }
[Parameter] public IList<TItem> Data { get; set; }
[Parameter] public string TextProperty { get; set; }
}
and then I'm pulling that through into my Edit.cshtml Razor page like this:
<div class="form-group">
<label>Line Manager(s)</label>
<component type="typeof(Dropdown<UserDto>)" render-mode="ServerPrerendered" param-BindValue="#Model.Input.SelectedLineManagers"
param-Data="#Model.Input.LineManagers" param-TextProperty="FullName" />
</div>
This works in that it loads the component, pulls through a list of line managers and preselects any previously selected line managers (using data I've just input directly into the database). However, when I go to post the form, Input.SelectedLineManagers is always null - i.e. it doesn't appear to be binding properly when I update the dropdown.
What am I doing wrong? Using ASP.NET Core hosted Blazor Webassembly (.net 6).
Below is the code behind by Edit.cshtml file that gets called when the form is submitted.
public async Task<IActionResult> OnPostAsync(string userId)
{
//code omitted for brevity
var userManagerLinks = new List<UserManagerLink>();
if (Input.SelectedLineManagers != null)
{
foreach (var item in Input.SelectedLineManagers)
{
userManagerLinks.Add(new UserManagerLink
{
UserId = int.Parse(userId),
ManagerId = item
});
}
}
var userSub = HttpContext.User?.FindFirstValue("Sub");
await _unitOfWork.SaveUser(userSub);
return RedirectToPage("Index");
}

IValidationAttributeAdapterProvider is called only for EmailAddressAttribute

What I was doing with ASP.NET MVC 5
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MaxLengthAttribute), typeof(MyMaxLengthAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute), typeof(MyRequiredAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MinLengthAttribute), typeof(MyMinLengthAttribute));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAddressAttribute), typeof(MyEmailAddressAttributeAdapter));
Now I'm migrating it to ASP.NET core 6
We can't use DataAnnotationsModelValidatorProvider anymore so I'm trying to use IValidationAttributeAdapterProvider, which doesn't work properly for me.
My codes
My IValidationAttributeAdapterProvider is below.
public class MyValidationAttributeAdapterProvider : ValidationAttributeAdapterProvider, IValidationAttributeAdapterProvider
{
IAttributeAdapter? IValidationAttributeAdapterProvider.GetAttributeAdapter(
ValidationAttribute attribute,
IStringLocalizer? stringLocalizer)
{
return attribute switch
{
EmailAddressAttribute => new MyEmailAddressAttributeAdapter((EmailAddressAttribute)attribute, stringLocalizer),
MaxLengthAttribute => new MyMaxLengthAttributeAdapter((MaxLengthAttribute)attribute, stringLocalizer),
MinLengthAttribute => new MyMinLengthAttribute((MinLengthAttribute)attribute, stringLocalizer),
RequiredAttribute => new MyRequiredAttributeAdapter((RequiredAttribute)attribute, stringLocalizer),
_ => base.GetAttributeAdapter(attribute, stringLocalizer),
};
}
}
My model class is below.
public class LogInRequestDTO
{
[Required]
[EmailAddress]
[MaxLength(FieldLengths.Max.User.Mail)]
[Display(Name = "mail")]
public string? Mail { get; set; }
[Required]
[MinLengthAttribute(FieldLengths.Min.User.Password)]
[DataType(DataType.Password)]
[Display(Name = "password")]
public string? Password { get; set; }
}
And in my Program.cs, I do like below.
builder.Services.AddControllersWithViews()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Resources));
});
builder.Services.AddSingleton<IValidationAttributeAdapterProvider, MyValidationAttributeAdapterProvider>();
What happed to me
I expect GetAttributeAdapter is called for each attribute like EmailAddressAttribute, MaxLengthAttribute, etc.
But it's called only once with EmailAddressAttribute.
So, all other validation results are not customized by my adaptors.
If I remove [EmailAddress] from the model class, GetAttributeAdapter is never called.
Am I missing something?
Added on 2022/05/24
What I want to do
I want to customize all the validation error message.
I don't want to customize for one by one at the place I use [EmailAddress] for example.
I need the server side validation only. I don't need the client side validation.
Reproducible project
I created the minimum sample project which can reproduce the problem.
https://github.com/KuniyoshiKamimura/IValidationAttributeAdapterProviderSample
Open the solution with Visual Studio 2022(17.2.1).
Set the breakpoint on MyValidationAttributeAdapterProvider.
Run the project.
Input something to the textbox on the browser and submit it.
The breakpoint hits only once with EmailAddressAttribute attribute.
The browser shows the customized message for email and default message for all other validations.
Below is a work demo, you can refer to it.
In all AttributeAdapter, change your code like below.
public class MyEmailAddressAttributeAdapter : AttributeAdapterBase<EmailAddressAttribute>
{
// This is called as expected.
public MyEmailAddressAttributeAdapter(EmailAddressAttribute attribute, IStringLocalizer? stringLocalizer)
: base(attribute, stringLocalizer)
{
//attribute.ErrorMessageResourceType = typeof(Resources);
//attribute.ErrorMessageResourceName = "ValidationMessageForEmailAddress";
//attribute.ErrorMessage = null;
}
public override void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-must-be-true", GetErrorMessage(context));
}
// This is called as expected.
// And I can see the message "Input the valid mail address.".
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
}
In homecontroller:
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult Index([FromForm][Bind("Test")] SampleDTO dto)
{
return View();
}
Index view:
#model IV2.Models.SampleDTO
#{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<h4>SampleDTO</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Index">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Test" class="control-label"></label>
<input asp-for="Test" class="form-control" />
<span asp-validation-for="Test" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Result1:
Result2:
I found the solution.
What I have to use is not ValidationAttributeAdapterProvider but IValidationMetadataProvider.
This article describes the usage in detail.
Note that some attributes including EmailAddressAttribute have to be treated in special way as describe here because they have default non-null ErrorMessage.
I confirmed for EmailAddressAttribute and some other attributes.
Also, there's the related article here.

passing blazor parameters to another page

I have been trying to pass parameters trough another page and this works, however i'm not getting what I desired and it has probably to do with what i pass.
The first thing i pass is a name but includes spaces and special character, the second thing i pass is a web link
how i send it:
<div class="col-sm-4">
<h3>Programming</h3>
#if (programming == null)
{
<p><em>Loading...</em></p>
}
else
{
foreach (var program in programming)
{
#program.Name
<br />
}
}
</div>
where it goes to
#page "/CourseDetails"
#using Portfolio.Models;
#using Portfolio_Frontend.Data;
#using Microsoft.AspNetCore.WebUtilities
#inject NavigationManager NavigationHelper
<h3>CourseDetails</h3>
#if (Name == null)
{
<p><em>Loading...</em></p>
}
else
{
<p>#Name</p>
}
#code {
public string Name { get; set; }
protected override void OnInitialized()
{
var uri = NavigationHelper.ToAbsoluteUri
(NavigationHelper.Uri);
if (QueryHelpers.ParseQuery(uri.Query).
TryGetValue("name", out var name))
{
Name = name.First();
}
}
}
i tried parameters as well and now tried query string gives the same result.
the name it should pass in this particular case is: C# Intermediate: Classes, Interfaces and OOP
What i get is only 'C' I assume because it is not able to translate the #.
is there a way to pass literal strings?
where it goes to: https://localhost:5105/CourseDetails/?name=C#%20Intermediate:%20Classes,%20Interfaces%20and%20OOP
this seems right to me.
Minor correction of URL syntax methodology
You have:
#program.Name
Which has a URL of /CourseDetails/?name=C#
Normally, you would do either
/CourseDetails/C#
/CourseDetails?name=C#
Except, Blazor doesn't explicitly support optional route parameters (/CourseDetails?name=C#)
REF: https://blazor-university.com/routing/optional-route-parameters/#:~:text=Optional%20route%20parameters%20aren%E2%80%99t%20supported%20explicitly%20by%20Blazor,,then%20replace%20all%20references%20to%20currentCount%20with%20CurrentCount.
It looks as though you can keep the optional query parameters and fiddle with the QueryHelpers.ParseQuery() I don't quite buy into that but if you want to keep going that route check out this post by #chris sainty
Link: https://chrissainty.com/working-with-query-strings-in-blazor/
I would much rather create a new model (DTO) that knows exactly how to display the CourseDetails name in a URL encoded fashion for the link, and the display name for the user.
public class ProgramModel
{
private readonly string name;
public ProgramModel(string name)
{
this.name = name;
}
public string DisplayName => name;
public string RelativeUrl => HttpUtility.UrlEncode(name);
}
And when we need to render the links on the 'Courses' page, it would look like this:
#page "/courses"
#using BlazorApp1.Data
<div class="col-sm-4">
<h3>Programming</h3>
#foreach (var program in programming)
{
#program.DisplayName
<br />
}
</div>
#code {
public IEnumerable<ProgramModel> programming { get; set; }
protected override void OnInitialized()
{
programming = new List<ProgramModel>()
{
new ProgramModel("Rust Things"),
new ProgramModel("JavaScript Things"),
new ProgramModel("C# Things")
};
}
}
And finally, when displaying the CourseDetails page, we can simply decode the name from the URL with the same utility that encoded the string in the first place, instead of guessing whether or not it's the apps fault, or the browsers fault that the '#' is not getting encoded properly to '%23'
#page "/CourseDetails/{Name}"
#inject NavigationManager NavigationHelper
#using System.Web
<h3>CourseDetails</h3>
<p>#HttpUtility.UrlDecode(Name)</p>
#code {
[Parameter]
public string Name { get; set; }
}
I recommend letting go of the idea of navigating from page to page, and using components:
<div>
#if (SelectedItem is not null)
{
<MyResultsPage SelectedProgramClass=#SelectedItem />
}
</div>
#code
{
ProgramClass SelectedItem {get; set;}
void SomeWayToSelectMyItem(ProgramClass newSelection){
SelectedItem = newSelection;
StateHasChanged();
}
}
Then in your display page, MyResultsPage.blazor
<div>
<div>#SelectedProgramClass.name</div>
. . .
</div>
#code {
[Parameter]
ProgramClass SelectedProgramClass{get; set;}
}
<MyResultsPage> will not show up in any way on the client, or even be initialized, until you've assigned something to SelectedProgramClass.

Razor Pages - Return Error on Duplicate Name

I'm working on a Razor Pages form that takes in a string to create a new customer in a SQL Server Database. I want to make it work so that if the string that is the customer already exists, a prompt comes up that says "This Customer Already Exists". Just to be safe for data integrity.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
int customerCheck = -1; //No Customer ID is -1
try
{
using (var context = new DataWarehouseContext())
{
customerCheck = context.Customer //Tries to grab a Customer with this name
.Where(a => a.Name == Customer.name)
.Select(b => b.CustomerId)
.FirstOrDefault();
}
}
catch (Exception)
{
}
if(customerCheck == -1)
{
_context.Customer.Add(Customer);
await _context.SaveChangesAsync();
return RedirectToPage("/Customer/List");
}
else
{
return Page();
}
}
This is the code I have so far in my backend. What happens is that when a user tries to create a new customer, the backend of the page tries to see if it can grab a customer ID that correlates to this name. If it can, then the value of customerCheck is not -1, therefore some error should get printed out.
I don't know what methods can be used to do this, so any help would be great!
I found a solution, and it wasn't hard to implement. When a duplicate customer was found in the backend, I create a ModelState.AddModelError object and fill it with a key and a description of the error. Next, in the frontend, I put it within an H3 tag to print it out like so:
Backend OnPost() Code
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
int customerCheck = 0; //No Customer ID is 0
try
{
using (var context = new DataWarehouseContext())
{
customerCheck = context.Customer //Tries to grab a Customer with this name
.Where(a => a.Name == Customer.name)
.Select(b => b.CustomerId)
.FirstOrDefault();
}
}
catch (Exception)
{
}
if(customerCheck == 0)
{
_context.Customer.Add(Customer);
await _context.SaveChangesAsync();
return RedirectToPage("/Customer/List");
}
else
{
ModelState.AddModelError("DuplicateCustomer", "This Customer Already Exists");
return Page();
}
}
So on the frontend, it gets implemented like this:
<h3 align="center" style="color:yellowgreen">#Html.ValidationMessage("DuplicateCustomer")</h3>
When return Page(); is hit, the page is reloaded and the DuplicateCustomer Error appears.
At first, glad to hear you have found a solution.
Besides, I think you could also use the Remote Validation to check whether the Customer is exist or not. Check the following sample code:
Remote validation in ASP.NET (Core) relies on Unobtrusive AJAX, so you will need to install that first. The easiest way to do this is via LibMan. Right click on the lib folder in wwwroot, choose Add ยป Client-side Library, and then choose jsdelivr as the source, and type in jquery-ajax-unobtrusive, click the "Install" button to install the package.
In the CreateCustomer.cshtml.cs page, add a Email property and use the PageRemote attribute, then, add a handler method to perform the validation.
public class CreateCustomerModel : PageModel
{
private readonly IRepository _repository;
public CreateCustomerModel(IRepository repository)
{
_repository = repository;
}
[PageRemote(ErrorMessage = "Email Address already exists", AdditionalFields = "__RequestVerificationToken", HttpMethod = "post",PageHandler = "CheckEmail")]
[BindProperty]
public string Email { get; set; }
public void OnGet()
{
}
public IActionResult OnPost()
{
if (ModelState.IsValid)
{
//insert data into database.
}
return Page();
}
#pragma warning disable MVC1001 // Filters cannot be applied to page handler methods.
[ValidateAntiForgeryToken]
#pragma warning restore MVC1001 // Filters cannot be applied to page handler methods.
public JsonResult OnPostCheckEmail()
{
//query database and check whether the email is exist or not.
var existingEmails = _repository.GetCustomers().Select(c => c.Email.ToLower()).ToList();
var valid = !existingEmails.Contains(Email.ToLower());
return new JsonResult(valid);
}
In the CreateCustomer.cshtml razor page, add JQuery reference and add a form to enter the values.
#page
#model RazorSample.Pages.CreateCustomerModel
#{
}
<div class="row">
<div class="col-md-4">
<form method="post" asp-antiforgery="true">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
#* add other fields *#
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
#section scripts{
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<partial name="_ValidationScriptsPartial" />
<script src="~/lib/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js"></script>
}
After submit the button, the result as below: if the email is exist, it will show the prompt:
[Note] In the above sample, we are adding the properties in the PageModel (instead of nested model), and use it to validate the field. Because, if using nested object, we might meet the 400 Bad Request result. The 400 error is related to the AntiForgeryToken, if you meet this error, try to ignore validate the AntiForgeryToken or custom add the __RequestVerificationToken token at the body or header, check this link.
More detail information about Remote Validation in Razor Pages, check the following articles:
Remote Validation in Razor Pages
Improved Remote Validation in Razor Pages

How to reset value in a cascading dropdown in .NET Core Razor Pages

I'm new to whole .NET Core thing, I was using ASP.NET 4 (Forms) for many year. I probably just can't think properly or I miss something obvious.
I have 3 classes:
public class Rat {
public int RatId { get; set; }
public string Name { get; set; }
public Color Color { get; set; }
public int ColorId { get; set; }
public ColorAddition ColorAddition {get;set;}
public int? ColorAdditionId { get; set; }
}
public class Color {
public int ColorId { get; set; }
public string Name { get; set; }
public bool Addition { get; set; }
}
public class ColorAddition {
public int ColorAdditionId { get; set; }
public string Name { get; set; }
}
Every rat must have a color and can have color addition. Some Colors have Color Additions (all colors with color additions have the same subset of color additions). If the rat have color with color addition, you need to specify that addition, otherwise it can be null.
I created CRUD for the rat class. The update view looks like this:
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Rat.RatId" />
<div class="form-group">
<label asp-for="Rat.Name" class="control-label"></label>
<input asp-for="Rat.Name" class="form-control" />
<span asp-validation-for="Rat.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Rat.ColorId" class="control-label"></label>
<select asp-for="Rat.ColorId" class="form-control" asp-items="ViewBag.ColorId"></select>
<span asp-validation-for="Rat.ColorId" class="text-danger"></span>
</div>
<div class="form-group">
<div id="ColorAddition">
<label asp-for="Rat.ColorAdditionId" class="control-label"></label>
<select asp-for="Rat.ColorAdditionId" class="form-control" asp-items="ViewBag.ColorAdditionId">
<option disabled selected>Zvolte</option>
</select>
<span asp-validation-for="Rat.ColorAdditionId" class="text-danger"></span>
</div>
</div>
It consists of two select lists, one for Color, another one for color addition. When the color have color addition, I would like to display Color addition list, otherwise it should be hidden.
So I created this at the end of the view:
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$('#Rat_ColorId').change(function () {
var selectedColor = $("#Rat_ColorId").val();
$.getJSON(`?handler=HaveColorAddition&id=${selectedColor}`, function (emp) {
console.log(emp);
if (emp == true)
{
$('#ColorAddition').show();
}
else
{
$('#ColorAddition').hide();
$('#Rat_ColorAdditionId').val(null);
}
});
});
</script>
}
It calls Json, which is returning true if the color have color addition and false if not:
public JsonResult OnGetHaveColorAddition(int id)
{
return new JsonResult(_context.Colors.Find(id).Addition);
}
So far so good, this is working as intended. If I choose color without color addition, the color addition select list is hidden and its value is null.
The trouble is that if I update the form now, color addition is not empty, but it keeps the previous value.
Example: I have blue color (with color addition light and dark) and black color (without color addition). Right now the color is blue and addition is light. I want to change color to black, so I choose black. Select list with addition is hidden now and without a value. Once I submit the form, color is changed to black but addition is still light.
Update code looks like this:
public async Task<IActionResult> OnPostAsync(int? id)
{
if (!ModelState.IsValid)
{
return Page();
}
var ratToUpdate = await _context.Rats.FindAsync(id);
if (ratToUpdate == null)
{
return NotFound();
}
if (await TryUpdateModelAsync<Rat>(
ratToUpdate,
"rat",
r => r.Name, r =>r.ColorId, r => r.ColorAdditionId))
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
return Page();
}
What am I doing wrong?
When you post data to handler , you just post Rate id to server side , and still query the database to base on id :
var ratToUpdate = await _context.Rats.FindAsync(id);
So that the ColorAdditionId is always the one which stored in database . You should include the Rat.ColorAdditionId when page posting data to OnPostAsync handler , so that server side can update the ColorAdditionId and save to database , and show new value after return RedirectToPage("./Index"); which will query the datbabase to get the newest value .
Please refer to below documents for how to use Forms in Razor Pages :
https://www.learnrazorpages.com/razor-pages/forms
https://learn.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-3.1&tabs=visual-studio