Convert input control to bind to multiple types - Blazor (.net v5) - asp.net-core

Im attempting to create an input element that I can use inside AND outside an EditForm component as Chris Sainty explored in this excellent blog post.
The difference is I would like to create an input that can also be bound to any type.
The following code i tried for the child component:
#typeparam T
<input value="#Value" #oninput="HandleInput" />
#code {
private FieldIdentifier _fieldIdentifier;
[Parameter] public T Value { get; set; }
[Parameter] public EventCallback<T> ValueChanged { get; set; }
[Parameter] public Expression<Func<T>> ValueExpression { get; set; }
[CascadingParameter] private EditContext CascadedEditContext { get; set; }
protected override void OnInitialized()
{
_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
}
private async Task HandleInput(ChangeEventArgs args)
{
#* How do I get args into a T type?
await ValueChanged.InvokeAsync(args.Value);*#
CascadedEditContext?.NotifyFieldChanged(_fieldIdentifier);
}
}
But what im struglling with is how can we convert args in the HandleInput function to type T?
Any ideas will be really appreciated!

Ok if this helps anyone else, ive tested a few types including nullables and replacing HandleInput with this works:
private async Task HandleInput(ChangeEventArgs args)
{
T argsValue = (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(args.Value.ToString());
await ValueChanged.InvokeAsync(argsValue);
CascadedEditContext?.NotifyFieldChanged(_fieldIdentifier);
}
Note - you will just need to handle errors converting types.

Related

ASP.NET CORE 6 - Blazor - How can we infer the component type for child components from a method?

The method which is used is ItemsProvider. Here are the 2 components.
Column.razor
#typeparam TTT
#code {
[CascadingParameter] public Grid<TTT>? MyGrid { get; set; }
protected override void OnInitialized()
{
MyGrid?.Add(this);
}
}
Grid.razor
#attribute [CascadingTypeParameter(nameof(TTT))]
#typeparam TTT
<CascadingValue Value="this">
#Columns
</CascadingValue>
#code {
[Parameter] public List<TTT>? Items { get; set; }
[Parameter] public RenderFragment? Columns { get; set; }
[Parameter] public Func<Task<IEnumerable<TTT>>>? ItemsProvider { get; set; }
private readonly List<Column<TTT>> MyColumns = new();
public void Add(Column<TTT> column)
{
MyColumns.Add(column);
}
}
When using these 2 components on the Index page below, I got the following error messages:
RZ10001 The type of component 'Column' cannot be inferred based on the values provided.
Consider specifying the type arguments directly using the following attributes: 'TTT'.
CS0411 The type arguments for method 'TypeInference.CreateColumn_1(RenderTreeBuilder, int)'
cannot be inferred from the usage. Try specifying the type arguments explicitly.
<Grid ItemsProvider="#Provide">
<Columns>
<Column></Column>
</Columns>
</Grid>
#code {
List<Test> tests = new();
async Task<IEnumerable<Test>> Provide()
{
await Task.Delay(1);
return new List<Test>() { new Test() { Name = "Name" } };
}
}
The problem can be fixed by:
A) Define the type explicitly for the Column:
<Column TTT="Test">
B) Define the Items parameter for the Grid:
<Grid ItemsProvider="#Provide" Items="tests">
C) Use the ItemsProvider delegate defined for the official Virtualize component.
Is there another solution that avoids A, B and C?
PS: Column.razor and Grid.razor are in a library.

Microsoft.AspNetCore.Components.Forms.InputRadioGroup` does not support the type xxx

I want to use radio group in blazor so after implementing edit form and select one of the radio button I got this error :
Microsoft.AspNetCore.Components.Forms.InputRadioGroup`1[EGameCafe.SPA.Models.GameModel] does not support the type 'EGameCafe.SPA.Models.GameModel'.
here is my edit form :
<EditForm Model="ViewModel" OnValidSubmit="HandleCreateGroup">
#if (ViewModel.Games.List.Any())
{
<InputRadioGroup Name="GameSelect" #bind-Value="Gamemodelsample">
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game" />
#game.GameName
<br />
}
</InputRadioGroup>
}
</EditForm>
#code{
public GameModel GameModelSample { get; set; } = new();
}
and GameModel is :
public class GameModel
{
public string GameId { get; set; }
public string GameName { get; set; }
}
The InputRadioGroup, like other Blazor components, supports only a limited amount of types like String or Int32. You had the right idea, but unfortunately, you run into a kind of limitation of Blazor.
You could try to create a wrapper field.
private String _selectedGameId = "<Your Default Id>";
public String SelectedGameId
{
get => _selectedGameId;
set
{
_selectedGameId = value;
// Set the property of the ViewModel used in your Model Property of the EditContext or any other property/field
ViewModel.SelectedGame = ViewModel.Games.List?.FirstOrDefault(x => x.GameId == value);
}
}
Use the property SelectedGameId as the bind value of the InputRadioGroup component.
<InputRadioGroup Name="GameSelect" #bind-Value="SelectedGameId" >
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game.GameId" />
#game.GameName
<br />
}
</InputRadioGroup>
As an alternative, you can create a custom component that inheriting from InputRadioGroup to create a kind of GameBasedInputRadioGroup. If you are interested I can post a sample.
Because in your code #bind-Value="Gamemodelsample",you are trying to bind GameName(string) to Gamemodelsaple(object), which will cause type mismatch problems.
You only need to modify your code to:
#bind-Value="GameModelSample.GameName"

How to generate a Razor Page url within a custom TagHelper

I have a custom tag helper which should render something like this:
<ol>
<li>Some text
</ol>
If I were to do this within a Razor Page I would simply do something like this: <a asp-page="MyRazorPage">Some text</a>
Is there a way to do something similar inside of the TagHelper?
I found the answer.
Inject IUrlHelperFactory into the constructor as well as use the following property:
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
Then you can create an IUrlHelper this way:
var urlHelper = _urlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.Page("/Clients/Edit", new { Id = myClientId });
output.Content.AppendHtmlLine($"<a href='{url}'>Edit</a>");
TagHelper provides HtmlTargetElement to add attributes to specified tags. Take adding asp-cuspage to the tag <a> as an example. The method Init is used to receive the parameters in the instruction asp-cuspage="". This method Process provides output attributes.
Create class CusAnchorTagHelper:
[HtmlTargetElement("a")]
public class CusAnchorTagHelper : TagHelper
{
private const string CuspageAttributeName = "asp-cuspage";
[HtmlAttributeName(CuspageAttributeName)]
public string Cuspage { get; set; }
public string Value { get; set; }
public override void Init(TagHelperContext context)
{
if (context.AllAttributes[0].Value != null)
{
Value = context.AllAttributes[0].Value.ToString();
}
base.Init(context);
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var reg = new Regex("(?<!^)(?=[A-Z])");
string attr="";
foreach(var a in reg.Split(Value))
{
attr += a + "/";
}
output.Attributes.SetAttribute("href", attr);
}
}
Then, inject custom taghelper assembly into the page. And it will be drawn in the view.
This is the rendered result.

Blazor sanitize MarkupString

I'm trying to sanitize content of MarkupString. Actually I created this (based from https://github.com/dotnet/aspnetcore/blob/574be0d22c1678ed5f6db990aec78b4db587b267/src/Components/Components/src/MarkupString.cs)
public struct MarkupStringSanitized
{
public MarkupStringSanitized(string value)
{
Value = value.Sanitize();
}
public string Value { get; }
public static explicit operator MarkupStringSanitized(string value) => new MarkupStringSanitized(value);
public override string ToString() => Value ?? string.Empty;
}
But render output isn't raw html. How should I implement MarkupStringSanitized to use
#((MarkupStringSanitized)"Sanitize this content")
Couple of suggestions (Not necessarily for OP, but for anyone else looking to solve the problem):
You didn't provide the code that does the actual sanitization, so I'm going to state the (hopefully) obvious best practice and if you're following it, great. Do not use Regular Expressions (Regex) to parse HTML
Also, the Sanitize() method should follow the pattern of immutability in this case
I would suggest the following library Gans.XSS.HtmlSanitizer which is an active library and updated regularly.
The problem
Razor View Engine can doesn't know how to render a MarkupStringSanitized. Just because you duck typed a sanitized version of the same struct doesn't mean it can render it. To get this to render, you'll need to cast it to something it does know, MarkupString
Here's what happens when I used your HtmlSanitizedMarkup directly with no modifications.
#((MarkupStringSanitized)Content)
Working Example #1
Here's an example using my Markdown -> Html playground (fully tested and working):
MarkupStringSanitized.cs
public struct MarkupStringSanitized
{
public MarkupStringSanitized(string value)
{
Value = Sanitize(value);
}
public string Value { get; }
public static explicit operator MarkupStringSanitized(string value) => new MarkupStringSanitized(value);
public static explicit operator MarkupString(MarkupStringSanitized value) => new MarkupString(value.Value);
public override string ToString() => Value ?? string.Empty;
private static string Sanitize(string value) {
var sanitizer = new HtmlSanitizer();
return sanitizer.Sanitize(value);
}
}
MarkupStringSanitizedComponent.razor
#if (Content == null)
{
<span>Loading...</span>
}
else
{
#((MarkupString)(MarkupStringSanitized)Content)
}
#code {
[Parameter] public string Content { get; set; }
}
That extra conversion though is ugly IMO. (maybe someone smarter than me can clean that up?)
Working example #2
Here I tried extending the MarkupString with an extension method. It looks a little better, but only a little.
MarkupStringExtensions.cs
public static class MarkupStringExtensions
{
public static MarkupString Sanitize(this MarkupString markupString)
{
return new MarkupString(SanitizeInput(markupString.Value));
}
private static string SanitizeInput(string value)
{
var sanitizer = new HtmlSanitizer();
return sanitizer.Sanitize(value);
}
}
MarkupStringSanitizedComponent.razor
#if (Content == null)
{
<span>Loading...</span>
}
else
{
#(((MarkupString)Content).Sanitize())
}
#code {
[Parameter] public string Content { get; set; }
}

How to add new language to ABP template?

I'm using free boilerplate (ASP.NET Core MVC & jQuery) from this site https://aspnetboilerplate.com/Templates
Is it possible to add new language support?
I already add localized .xml file, update 'abplanguages' table in database but it is not working. I'm changing language but text is still in english. The same situation with predefined languages already shipped with boilerplate like 'espanol-mexico' is not working but when I pick 'french' the page is translated.
This is weird because in documentation said it can be done.
https://aspnetboilerplate.com/Pages/Documents/Localization#extending-localization-sources
I wonder is it free template restriction?
inject IApplicationLanguageManager interface and use AddAsync() method to add a new language.
private readonly IApplicationLanguageManager _applicationLanguageManager;
public LanguageAppService(
IApplicationLanguageManager applicationLanguageManager,
IApplicationLanguageTextManager applicationLanguageTextManager,
IRepository<ApplicationLanguage> languageRepository)
{
_applicationLanguageManager = applicationLanguageManager;
_languageRepository = languageRepository;
_applicationLanguageTextManager = applicationLanguageTextManager;
}
protected virtual async Task CreateLanguageAsync(ApplicationLanguageEditDto input)
{
if (AbpSession.MultiTenancySide != MultiTenancySides.Host)
{
throw new UserFriendlyException(L("TenantsCannotCreateLanguage"));
}
var culture = CultureHelper.GetCultureInfoByChecking(input.Name);
await _applicationLanguageManager.AddAsync(
new ApplicationLanguage(
AbpSession.TenantId,
culture.Name,
culture.DisplayName,
input.Icon
)
{
IsDisabled = !input.IsEnabled
}
);
}
public static class CultureHelper
{
public static CultureInfo[] AllCultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
public static bool IsRtl => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft;
public static bool UsingLunarCalendar = CultureInfo.CurrentUICulture.DateTimeFormat.Calendar.AlgorithmType == CalendarAlgorithmType.LunarCalendar;
public static CultureInfo GetCultureInfoByChecking(string name)
{
try
{
return CultureInfo.GetCultureInfo(name);
}
catch (CultureNotFoundException)
{
return CultureInfo.CurrentCulture;
}
}
}
public class ApplicationLanguageEditDto
{
public virtual int? Id { get; set; }
[Required]
[StringLength(ApplicationLanguage.MaxNameLength)]
public virtual string Name { get; set; }
[StringLength(ApplicationLanguage.MaxIconLength)]
public virtual string Icon { get; set; }
/// <summary>
/// Mapped from Language.IsDisabled with using manual mapping in CustomDtoMapper.cs
/// </summary>
public bool IsEnabled { get; set; }
}
I figure it out. In my case it was incorrect build action property. In VS right click on localization source file: *.xml file -> Advanced -> Build action: Embedded resource.