How to set focus on Blazor.Typeahead component? - asp.net-core

I'm using the BlazorTypeahead component in my project. I would like to set focus on the typeahead textbox, but can't seem to figure out how to do it. Here's my page. The search and value changed methods work fine, so I'm leaving them out.
#page "/"
#using Microsoft.JSInterop
#using Microsoft.AspNetCore.Components
#inject IJSRuntime jsRuntime
#inject Blazored.LocalStorage.ILocalStorageService localStore
<BlazoredTypeahead SearchMethod="SearchMyModel" TItem="MyModel" TValue="MyModel" Value="SelectedMyModel" ValueChanged="MyModelChanged" ValueExpression="#(() => SelectedMyModel)" placeholder="My Model name..." #ref="NewElementHere">
<SelectedTemplate>
#context.Name
</SelectedTemplate>
<ResultTemplate>
#context.Name (#context.AnotherProperty)
</ResultTemplate>
</BlazoredTypeahead>
#code {
//public BlazoredTypeahead<MyModel, MyModel> NewElementHere { get; set; }
ElementReference NewElementHere;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Focus the element
await jsRuntime.InvokeAsync<object>("BlazorFocusElement", NewElementHere);
}
}
}
The index.html file has this script in the header.
window.BlazorFocusElement = (element) => {
if (element instanceof HTMLElement) {
element.focus();
}
};
The code above produces the following compile time error:
Error CS0029 Cannot implicitly convert type
'Blazored.Typeahead.BlazoredTypeahead<MyModel, MyModel>' to
'Microsoft.AspNetCore.Components.ElementReference'
If I remove the ElementReference and instead enable [i.e., remove comment] the property in the #code, it'll build, but I get a runtime error An unhandled error has occurred. If I look in the web debugger console it says:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Derived classes must implement it System.NotImplementedException: Derived classes must
implement it

You can't apply ElementReference to component... BlazoredTypeahead is a component, so you can't do it. The author of the BlazoredTypeahead should have provided a way to do it... Review the methods and properties of this component. Perhaps this component provide such functionality via one of its attributes...
In any case, you can't use the ElementReference here. But I guess you can still use JSInterop to set the focus, even if the input text you want to set focus to has no id attribute. Just look at the Html source, identify the input text element, and contrive a way to set the focus.
Note that if you're using .Net 5.0, you can set the focus from Blazor.

Related

Identify which MudExpansionPanel is being expanded

I want to present a list of up to 20 panels within a <MudExpansionPanels> component where the expanded child portion of each <MudExpansionPanel> is expensive to render. I tried the following test code but all instances of <LiveAgentSummary> are rendered as the parent is rendered, just to clarify this rendering of <LiveAgentSummary> happens before any panel is manually expanded.
<MudExpansionPanels>
#foreach (var liveAgent in _liveAgents)
{
<MudExpansionPanel Text=#liveAgent.Name>
<LiveAgentSummary AgentId=#liveAgent.Id />
</MudExpansionPanel>
}
</MudExpansionPanels>
I then looked into delaying the render of each <LiveAgentSummary> through use of a RenderFragment that is dynamically built during the <MudExpansionPanel> IsExpandedChanged event. However the event handler does not indicate which panel is being expanded and hence I do not know which liveAgent.Id param value to pass to <LiveAgentSummary> as I build a RenderFragment.
I think <MudExpansionPanels> is missing support for a bind-ActivePanelId property but hopefully I am overlooking an alternative solution to my delayed rendering objective.
This is the official MudBlazor example that prompted me to look into using a RenderFragment.
Update: A long answer briefly appeared yesterday suggesting that I could query the list of panel components on a built-in property that indicates the expanded state. The poster had gone to the trouble of reading the MudBlazor source code but the answer was then deleted.
I am now wondering how from code in an event handler it is possible to iterate over a component hierarchy declared as mark-up. Applying this to my example markup above, how could event handler code obtain a reference to each <MudExpansionPanel> child within <MudExpansionPanels>.
Can't you make use of the bool from the IsExpandedChanged callback? Something like this:
Index.razor
#page "/"
<MudExpansionPanels>
#foreach (var liveAgent in this.liveAgents)
{
<MudExpansionPanel
Text="#($"{liveAgent.Name} ({liveAgent.Data})")"
IsExpandedChanged="#(e => this.Load(e, liveAgent))">
<LiveAgentSummary Agent="#liveAgent" />
</MudExpansionPanel>
}
</MudExpansionPanels>
#code {
private readonly List<Agent> liveAgents = new()
{
new Agent("1", "Agent Smith"),
new Agent("2", "Agent Brown"),
new Agent("3", "Agent Jones")
};
private void Load(bool expanded, Agent agent)
{
if (expanded)
{
agent.Load();
}
}
}
LiveAgentSummary.razor
<MudText>id: #this.Agent.Id, data: #this.Agent.Data</MudText>
#code {
[Parameter]
public Agent Agent { get; set; } = default!;
}
Agent.cs
public record Agent(string Id, string Name)
{
public string Data { get; set; } = "Not loaded";
public void Load()
{
Console.WriteLine($"Loading agent {this.Id}...");
this.Data = "Loaded!";
}
}

Can I add to or change the default CSS class when using ValidationMessage in ASP.NET Core?

I am using ValidationMessage in a razor component to show validation message, like this:
<ValidationMessage For="#(() => ViewModel.CompanyNumber)" />
This generates this HTML code:
<div class="validation-message">The company number field is required.</div>
Is it possible to change the CSS-class? I want to use something else than validation-message. Adding class="myclass" is ignored by the controller. I've also tried with #attributes without success.
With .NET5 they added functionality to customize the validation classes on the actual input-fields (which issue 8695 was about) by way of setting a FieldCssClassProvider to the edit context. But there still seems to be no way of customizing the classes of the ValidationSummary or ValidationMessage components
Snipped directly from the .NET 5 docs
var editContext = new EditContext(model);
editContext.SetFieldCssClassProvider(new MyFieldClassProvider());
...
private class MyFieldClassProvider : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext,
in FieldIdentifier fieldIdentifier)
{
var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
return isValid ? "good field" : "bad field";
}
}
Using this will yield the below html for an invalid input. At least with this we can style the actual input elements. Just not the messages...
<input class="bad field" aria-invalid="">
<div class="validation-message">Identifier too long (16 character limit).</div>
You can change the validation-message class inside the css file app.css inside the wwwroot. Or site.css in in earlier previews.
.validation-message {
color: red;
}
The class is set in ValidationMessage.cs
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
foreach (var message in CurrentEditContext.GetValidationMessages(_fieldIdentifier))
{
builder.OpenElement(0, "div");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", "validation-message");
builder.AddContent(3, message);
builder.CloseElement();
}
}
https://github.com/dotnet/aspnetcore/blob/master/src/Components/Web/src/Forms/ValidationMessage.cs
Why don't you just copy the code for ValidationMessage.cs and write in your own property? There is nothing special about this class except for capturing a Cascading Parameter. Just take this file and make your own with a slightly different name then add:
[Parameter] public string AdditionalClassNames {get;set;}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
foreach (var message in CurrentEditContext.GetValidationMessages(_fieldIdentifier))
{
builder.OpenElement(0, "div");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", string.IsNullOrEmpty(AdditionalClassNames) ? "validation-message" : $"validation-message {AdditionalClassNames}");
builder.AddContent(3, message);
builder.CloseElement();
}
}
https://github.com/dotnet/aspnetcore/blob/master/src/Components/Web/src/Forms/ValidationMessage.cs
EDIT
Even better, it's not sealed! Just use it as a base class for a new version and add what I mentioned above.
It is not possible in ASP.NET Core 3.1. Hopefully, it will be included in next major version, see this feature request:
https://github.com/dotnet/aspnetcore/issues/8695

EditorFor Tag Helper doesn't render validation attributes when using FluentValidator

I have a simple form like this which makes use of the #Html.EditorFor extension:
<form method="post">
#Html.EditorFor(x => x.SystemSettings.EmailFromAddress)
<submit-button title="Save"></submit-button>
</form>
I want to take advantage of .NET Core's tag helpers so that my form looks like this instead:
<form method="post">
<editor asp-for="SystemSettings.EmailFromAddress"/>
<submit-button title="Save"></submit-button>
</form>
I also eventually would like to have my own custom tag helpers so I can do something like this instead:
<text-box asp-for="SystemSettings.EmailFromAddress"></text-box>
I have a string template which gets rendered by the #Html.EditorFor extension:
#model string
<div class="form-group">
<label asp-for="#Model" class="m-b-none"></label>
<span asp-description-for="#Model" class="help-block m-b-none small m-t-none"></span>
<div class="input-group">
<input asp-for="#Model" class="form-control" />
<partial name="_ValidationIcon" />
</div>
<span asp-validation-for="#Model" class="validation-message"></span>
</div>
To do that, I saw someone implemented an EditorTagHelper, which looks like this:
[HtmlTargetElement("editor", TagStructure = TagStructure.WithoutEndTag,
Attributes = ForAttributeName)]
public class EditorTagHelper : TagHelper
{
private readonly IHtmlHelper _htmlHelper;
private const string ForAttributeName = "asp-for";
private const string TemplateAttributeName = "asp-template";
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
[HtmlAttributeName(TemplateAttributeName)]
public string Template { get; set; }
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public EditorTagHelper(IHtmlHelper htmlHelper)
{
_htmlHelper = htmlHelper;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (output == null)
throw new ArgumentNullException(nameof(output));
if (!output.Attributes.ContainsName(nameof(Template)))
{
output.Attributes.Add(nameof(Template), Template);
}
output.SuppressOutput();
(_htmlHelper as IViewContextAware).Contextualize(ViewContext);
output.Content.SetHtmlContent(_htmlHelper.Editor(For.Name, Template));
await Task.CompletedTask;
}
}
When I use the EditorTagHelper though, it seems to be missing the unobtrusive Javascript validation attributes:
Using #Html.EditorFor, this gets rendered:
<input class="form-control valid" type="text" data-val="true" data-val-required="Email From Address cannot be empty" id="SystemSettings_EmailFromAddress" name="SystemSettings.EmailFromAddress" value="whatever#test.com" aria-required="true" aria-invalid="false" aria-describedby="SystemSettings_EmailFromAddress-error">
It's got the data-val attributes so client-side validation gets applied.
When I use the EditorTagHelper instead, this gets rendered:
<input class="form-control valid" type="text" id="SystemSettings_EmailFromAddress" name="SystemSettings.EmailFromAddress" value="whatever#test.com" aria-invalid="false">
The unobtrusive validation attributes are not being applied. I am using FluentValidation and I have specified an AbstractValidator like this:
public class SystemSettingsValidator : AbstractValidator<SystemSettings>
{
public SystemSettingsValidator()
{
RuleFor(x => x.EmailFromAddress).NotEmpty()
.WithMessage("Email From Address cannot be empty");
}
}
I found that if I removed the AbstractorValidator and simply added a [Required] attribute to my model property the validation then works properly. This suggests that there is something wrong with FluentValidation. Perhaps there is a configuration issue.
I am using Autofac dependency injection to scan my assemblies and register validator types:
builder.RegisterAssemblyTypes(Assembly.Load(assembly))
.Where(t => t.IsClosedTypeOf(typeof(IValidator<>)))
.AsImplementedInterfaces()
.PropertiesAutowired()
.InstancePerLifetimeScope();
This seems to work fine. In case it wasn't fine, I also tried registering the validators from the fluent validation options like this:
.AddFluentValidation(fv =>
{
fv.RegisterValidatorsFromAssemblies(new List<Assembly>
{Assembly.GetExecutingAssembly(), Assembly.Load(nameof(Entities))});
})
This also seemed to be fine.
One thing to note is that an earlier problem I had was that using Autofac assembly scanning was breaking the application when tag helpers were included. I added a filter to ensure that tag helpers are not included when registering these dependencies, e.g.
builder.RegisterAutowiredAssemblyInterfaces(Assembly.Load(Web))
.Where(x => !x.Name.EndsWith("TagHelper"));
I have uploaded a working sample of the code here: https://github.com/ciaran036/coresample2
Navigate to the Settings Page to see the field I am trying to validate.
This issue also appears to affect view components.
Thanks.
I believe the issue is in the tag helper, in that it uses IHtmlHelper.Editor rather than IHtmlHelper<TModel>.EditorFor to generate the HTML content. They are not quite the same.
As you point out FluentValidation injects the validation attributes as you'd expect for #Html.EditorFor(x => x.SystemSettings.EmailFromAddress). However for #Html.Editor("SystemSettings.EmailFromAddress"), which is what your custom tag helper is doing, FluentValidation doesn't inject the validation attributes. So that rules out the tag helper itself and moves the problem to the Editor invocation.
I also noticed that Editor doesn't resolve <label asp-for (or the other <span asp-description-for tag helper you're using) so that suggests it's not a FluentValidation specific issue.
I wasn't able to replicate your success with the Required attribute for the custom tag helper/Editor - the Required attribute only injected the validation attributes when using EditorFor.
The internals for Editor and EditorFor are similar but with a key difference, the way they resolve the ModelExplorer instance used to generate the HTML content differs and I suspect this is the problem. See below for these differences.
Things like PropertyName set to null and Metadata.Property not being set for Editor, but set to EmailFromAddress and SystemSettings.EmailFromAddress for EditorFor are standing out as potential causes for the behaviour we're seeing.
The painful part is the tag helper has a valid ModelExplorer instance via the For property. But there is no built in provision to provide it to the html helper.
As to the resolution, the obvious one seems to be to use EditorFor rather than Editor however it doesn't look easy. It'd likely involve reflection and building an expression.
Another option is, considering the tag helper resolves the ModelExplorer correctly, is to extend HtmlHelper and override the GenerateEditor method - what both Editor and EditorFor end up invoking - so you can pass in the ModelExplorer and work around the problem.
public class CustomHtmlHelper : HtmlHelper, IHtmlHelper
{
public CustomHtmlHelper(IHtmlGenerator htmlGenerator, ICompositeViewEngine viewEngine, IModelMetadataProvider metadataProvider, IViewBufferScope bufferScope, HtmlEncoder htmlEncoder, UrlEncoder urlEncoder) : base(htmlGenerator, viewEngine, metadataProvider, bufferScope, htmlEncoder, urlEncoder) { }
public IHtmlContent CustomGenerateEditor(ModelExplorer modelExplorer, string htmlFieldName, string templateName, object additionalViewData)
{
return GenerateEditor(modelExplorer, htmlFieldName, templateName, additionalViewData);
}
protected override IHtmlContent GenerateEditor(ModelExplorer modelExplorer, string htmlFieldName, string templateName, object additionalViewData)
{
return base.GenerateEditor(modelExplorer, htmlFieldName, templateName, additionalViewData);
}
}
Update your tag helper to use it:
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (output == null)
throw new ArgumentNullException(nameof(output));
if (!output.Attributes.ContainsName(nameof(Template)))
{
output.Attributes.Add(nameof(Template), Template);
}
output.SuppressOutput();
(_htmlHelper as IViewContextAware).Contextualize(ViewContext);
var customHtmlHelper = _htmlHelper as CustomHtmlHelper;
var content = customHtmlHelper.CustomGenerateEditor(For.ModelExplorer, For.Metadata.DisplayName ?? For.Metadata.PropertyName, Template, null);
output.Content.SetHtmlContent(content);
await Task.CompletedTask;
}
Finally register the new helper, the earlier the better I'd say
services.AddScoped<IHtmlHelper, CustomHtmlHelper>();
Working solution

Validation messages from custom model validation attributes are locked to first loaded language

I am working on a multi lingual website using Umbraco 7.2.4 (.NET MVC 4.5). I have pages for each language nested under home nodes with their own culture:
Home (language selection)
nl-BE
some page
some other page
my form page
fr-BE
some page
some other page
my form page
The form model is decorated with validation attributes that I needed to translate for each language. I found a Github project, Umbraco Validation Attributes that extends decoration attributes to retrieve validation messages from Umbraco dictionary items. It works fine for page content but not validation messages.
The issue
land on nl-BE/form
field labels are shown in dutch (nl-BE)
submit invalid form
validation messages are shown in dutch (nl-BE culture)
browse to fr-BE/form
field labels are shown in french (fr-BE)
submit invalid form
Expected behavior is: validation messages are shown in french (fr-BE culture)
Actual behavior is: messages are still shown in dutch (data-val-required attribute is in dutch in the source of the page)
Investigation to date
This is not a browser cache issue, it is reproducible across separate browsers, even separate computers: whoever is generating the form for the first time will lock the validation message culture. The only way to change the language of the validation messages is to recycle the Application Pool.
I doubt that the Umbraco Validation helper class is the issue here but I'm out of ideas, so any insight is appreciated.
Source code
Model
public class MyFormViewModel : RenderModel
{
public class PersonalDetails
{
[UmbracoDisplayName("FORMS_FIRST_NAME")]
[UmbracoRequired("FORMS_FIELD_REQUIRED_ERROR")]
public String FirstName { get; set; }
}
}
View
#inherits Umbraco.Web.Mvc.UmbracoTemplatePage
var model = new MyFormViewModel();
using (Html.BeginUmbracoForm<MyFormController>("SubmitMyForm", null, new {id = "my-form"}))
{
<h3>#LanguageHelper.GetDictionaryItem("FORMS_HEADER_PERSONAL_DETAILS")</h3>
<div class="field-wrapper">
#Html.LabelFor(m => model.PersonalDetails.FirstName)
<div class="input-wrapper">
#Html.TextBoxFor(m => model.PersonalDetails.FirstName)
#Html.ValidationMessageFor(m => model.PersonalDetails.FirstName)
</div>
</div>
note: I have used the native MVC Html.BeginForm method as well, same results.
Controller
public ActionResult SubmitFranchiseApplication(FranchiseFormViewModel viewModel)
{
if (!ModelState.IsValid)
{
TempData["Message"] = LanguageHelper.GetDictionaryItem("FORMS_VALIDATION_FAILED_MESSAGE");
foreach (ModelState modelState in ViewData.ModelState.Values)
{
foreach (ModelError error in modelState.Errors)
{
TempData["Message"] += "<br/>" + error.ErrorMessage;
}
}
return RedirectToCurrentUmbracoPage();
}
}
LanguageHelper
public class LanguageHelper
{
public static string CurrentCulture
{
get
{
return UmbracoContext.Current.PublishedContentRequest.Culture.ToString();
// I also tried using the thread culture
return System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
}
}
public static string GetDictionaryItem(string key)
{
var value = library.GetDictionaryItem(key);
return string.IsNullOrEmpty(value) ? key : value;
}
}
So I finally found a workaround. In attempt to reduce my app to its simplest form and debug it, I ended up recreating the "UmbracoRequired" decoration attribute. The issue appeared when ErrorMessage was set in the Constructor rather than in the GetValidationRules method. It seems that MVC is caching the result of the constructor rather than invoking it again every time the form is loaded. Adding a dynamic property to the UmbracoRequired class for ErrorMessage also works.
Here's how my custom class looks like in the end.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
AllowMultiple = false)]
internal class LocalisedRequiredAttribute : RequiredAttribute, IClientValidatable
{
private string _dictionaryKey;
public LocalisedRequiredAttribute(string dictionaryKey)
{
_dictionaryKey = dictionaryKey;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata, ControllerContext context)
{
ErrorMessage = LanguageHelper.GetDictionaryItem(_dictionaryKey); // this needs to be set here in order to refresh the translation every time
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage, // if you invoke the LanguageHelper here, the result gets cached and you're locked to the current language
ValidationType = "required"
};
}
}

Understanding cakephp3 error handling

I want to create a maintenance Page for my cake website by checking a Database Table for a maintenance flag using a sub-function of my AppController "initilize()" method. If the flag is set, i throw my custom MaintenanceException(Currently containing nothing special):
class MaintenanceException extends Exception{
}
To handle it, I implemented a custom App Exception Renderer:
class AppExceptionRenderer extends ExceptionRenderer {
public function maintenance($error)
{
return "MAINTENANCE";
}
}
I am able to see this maintenance Text on my website if I set my DB flag to true, but I could not find any information in cake's error handling documentation (http://book.cakephp.org/3.0/en/development/errors.html) on how I can actually tell the Exception renderer to render view "maintenance" with Template "infopage".
Can I even us that function using the ExceptionRenderer without a custom error controller? And If not, how should a proper ErrorController implementation look like? I already tried this:
class AppExceptionRenderer extends ExceptionRenderer {
protected function _getController(){
return new ErrorController();
}
public function maintenance($error)
{
return $this->_getController()->maintenanceAction();
}
}
together with:
class ErrorController extends Controller {
public function __construct($request = null, $response = null) {
parent::__construct($request, $response);
if (count(Router::extensions()) &&
!isset($this->RequestHandler)
) {
$this->loadComponent('RequestHandler');
}
$eventManager = $this->eventManager();
if (isset($this->Auth)) {
$eventManager->detach($this->Auth);
}
if (isset($this->Security)) {
$eventManager->detach($this->Security);
}
$this->viewPath = 'Error';
}
public function maintenanceAction(){
return $this->render('maintenance','infopage');
}
}
But this only throws NullPointerExceptions and a fatal error. I am really dissapointed by the cake manual as well, because the code examples there are nowhere close to give me an impression of how anything could be done and what functionality I actually have.
Because I had some more time today, I spent an hour digging into the cake Source and found a solution that works well for me (and is propably the way it should be done, altough the cake documentation does not really give a hint):
Step 1: Override the _template(...)-Method of the ExceptionRenderer in your own class. In my case, I copied the Method of the parent and added the following Code at the beginning of the method:
$isMaintenanceException = $exception instanceof MaintenanceException;
if($isMaintenanceException){
$template = 'maintenance';
return $this->template = $template;
}
This tells our Renderer, that the error Template called "maintentance"(which should be located in Folder: /Error) is the Error Page content it should render.
Step 2: The only thing we have to do now (And its is kinda hacky in my opinion, but proposed by the cake documentation in this exact way) is to set the layout param in our template to the name of the base layout we want to render with. So just add the following code on top of your error template:
$this->layout = "infopage";
The error controller I created is actually not even needed with this approach, and I still don't know how the cake error controller actually works. maybe I will dig into this if I have more time, but for the moment.