I have created my own cached JSON string localizer by overriding IStringLocalizerFactory and IStringLocalizer and it works like a charm.
The only problem is that I can not localize data annotations because they seem to not work with a custom string localizer. I tried using the ResourceType property which should be used for exactly this case (at least as far as I am concerned), but instead of using my IStringLocalizer it tries to access properties. This might work with the default resx-localizer, but not with a custom one.
I could create properties in my class which reroute to my IStringLocalizer but thats a hassle and only want to do it this way if I have to.
My view model:
public class RegisterViewModel
{
[Required]
[Display(Name = "test", ResourceType = typeof(Lang))]
public string Username { get; set; }
}
is used inside of my cshtml like this:
<div class="form-group">
<label asp-for="Username" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Username" class="form-control" />
<span asp-validation-for="Username" class="text-danger"></span>
</div>
</div>
which should autogenerate the label and the textbox. But instead:
InvalidOperationException: Cannot retrieve property 'Name' because localization failed. Type 'Test.Models.Lang.Lang' is not public or does not contain a public static string property with the name 'test'.
booom. Thats my Lang class:
public class Lang
{
}
Instead of getting a IStringLocalizer<Lang> and searching for a test property via indexer (thats what I am trying to make him do), it tried to find the non-existing property test.
Is there any way to change the way the lookup works? I tried to changing the MvcDataAnnotationsLocalizationOptions via services.Configure(), but to no effect.
Or if it is possible to directly change the way the HTML-code is generated from those data-annotations?
Thank you for your help.
EDIT: This of course doesn't work because the CreateDisplayMetadata() is only called once and then cached, causing the first "translation" become the only one! I do have another solution extending the DefaultModelMetadataProvider or adding a IDisplayMetadataProvider (still not sure it's the right or best way to go...), so if anyone still needs help with this, please let me know :)
OLD AND WRONG ANSWER:
Don't know if there is any "more correct" (please let me know if so :) solution, but here is one that works in RC2:
public class LocalizedDisplayMetadataProvider : IDisplayMetadataProvider
{
public String Translate(String key)
{
if(key != null && key.StartsWith("#"))
{
return key.TrimStart('#').ToUpper(); // Todo: Replace with real localization code (e.g. fetch from json)...
}
else
{
return key;
}
}
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
foreach(var attr in context.Attributes.OfType<DisplayAttribute>().Where(a => a.ResourceType == null))
{
attr.Name = this.Translate(attr.Name);
attr.Description = this.Translate(attr.Description);
attr.GroupName = this.Translate(attr.GroupName);
attr.Prompt = this.Translate(attr.Prompt);
attr.ShortName = this.Translate(attr.ShortName);
}
}
}
And then register in ConfigureServices method:
services.Configure<MvcOptions>(m =>
{
m.ModelMetadataDetailsProviders.Add(new LocalizedDisplayMetadataProvider());
});
This will localize all strings starting with '#' (and not ResourceType set) on the DisplayAttribute, and can easily be expanded to handle the ValidationAttribute as well.
Related
So I am building a component library in blazor and I am adding support for the input components to have Blazor built in validation.
#inherits WindyInputTextBase;
#if(!string.IsNullOrEmpty(Label))
{
<label for="#Id" class="block text-sm font-medium">#Label</label>
}
<input class="mt-3 p-2 focus:outline-none focus:ring focus:ring-primary-light focus:border-primary block w-full shadow-sm sm:text-sm border border-gray-300 rounded-md #CssClass" id="#Id" name="#Id" #bind="#CurrentValue" />
<!--Div is empty when validation is not done.-->
<div class="text-danger text-sm mb-3">
<ValidationMessage For="#ValidationFor" />
</div>
The base class for the input component:
public class WindyInputTextBase : WindyInputBase<string>
{
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out string result, [NotNullWhen(false)] out string? validationErrorMessage)
{
result = value is null ? string.Empty : value;
validationErrorMessage = null;
return true;
}
}
And the base class of that one:
public abstract class WindyInputBase<T> : InputBase<T>
{
[Parameter]
public string? Label { get; set; }
[Parameter]
public Expression<Func<T>>? ValidationFor { get; set; }
}
The component (and others using the same base) work fine, but I get this error when using them outside an EditForm component:
Unhandled exception rendering component: WindyUI.Form.EditForm.WindyInputText requires a cascading parameter of type EditContext. For example, you can use WindyUI.Form.EditForm.WindyInputText inside an EditForm.
System.InvalidOperationException: WindyUI.Form.EditForm.WindyInputText requires a cascading parameter of type EditContext. For example, you can use WindyUI.Form.EditForm.WindyInputText inside an EditForm.
This is fine and expected, but forces me to write two sets of Input components, one that has validation and one that doesn't. I know MudBlazor does what I am looking to do, but I don't know how they do it exactly and their source code isn't all to clear for me since there is no inheritance from InputBase.
Is there a way to create a component with blazor built-in validation that can be used both inside and outside an EditForm component? Of course, this means that no built in validation is fine when not usig the components inside an EditForm.
Thanks,
Since I want to design some reusable Blazor components, I hope they could have a feature like this:
Suppose I have a custom component "MyComponent", I could add any CSS attribute to it when I am using it:
<MyComponent Class="custom-css1 custom-css2">
some child content...
</MyComponent>
While in MyComponent, I usually fix some common CSS attributes to the top wapper just like this:
<div class="fixed-css1 fixed-css2">
some child content...
</div>
That means I have to combine two parts of the CSS attributes together to make the final HTML like:
<div class="fixed-css1 fixed-css2 custom-css1 custom-css2">
some child content...
</div>
So I guess I should have this patern:
<div class="#Classes">
some child content...
</div>
#functions
{
[Parameter]
private string Class { get; set; } = "";
private string fixedClass = "fixed-css1 fixed-css2";
private string Classes
{
get
{
return $"{fixedClass} {Class}";
}
}
}
To reduce redundant code, I could make a base class that have a protected Class property and every component inherent from it, but I still can't avoid writing the same combine code in each one. I hope there is some solution to add those custom CSS directly in my base class, I guess I could achieve this by override BuildRenderTree method from ComponentBase clss:
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
}
But unfortunately, I have tried all my ways to build manually but have no idea to get it done. I don't know how to get elements of my HTML ("div" for example) and add additional CSS attributes to it.
All these are about doing a feature as Vue could easily do. In Vue code, we could certainly add any attribute to a component and pass them down to the first element in the component.
Could anybody help me complete this goal or give me some suggestion?
I think your approach is fine, it just needs a little abstraction around it to make it readable and easily managed across multiple components.
That's why I created this simple helper function library. It's exactly what you are doing in your code, but offers an API for consistency.
https://www.nuget.org/packages/BlazorComponentUtilities/
As far as I know, right now, Blazor does not offer a built-in way to handle CSS, and Blazor best practices and patterns are not yet available, so you can handle CSS in whatever manner you found fit, including JSInterop.
Below is a link to a library I believe can be very useful to you:
https://github.com/chanan/BlazorStyled
Hope this helps...
try this, i have used it and it works and is simple
<div class="fixed-css1 fixed-css2 #(Class)">
some child content...
</div>
I'm not sure if this is a bad practice,
It stops itellisense from working correctly in the class attribute,
but that is easily workable just add it last and at the end of the attribute if you need to make changes with intellisense add a "" before the #(class), make your changes and then remove the ""
it may leave a space in the class string if the Class parameter is not set on the component
eg <div class="fixed-css1 fixed-css2 "> (space at end)
You can do this in two ways.
The first way, you can use the dictionary to pass from parent to child like below. I have added the required attribute in this example.
Child Component:
<input id="firstName" #attributes="InputAttributes" />
#code {
[Parameter]
public Dictionary<string, object> InputAttributes { get; set; } =
new Dictionary<string, object>()
{
{ "placeholder", "Child Component Placeholder" }
};
}
Parent Component:
<ChildComponent InputAttributes="attributesFromParent">
</ChildComponent>
#code {
public Dictionary<string, object> attributesFromParent { get; set; } =
new Dictionary<string, object>()
{
{ "required", "required" },
{ "placeholder", "Parent Component Placeholder" }
};
}
The second way, you can do by setting CaptureUnmatchedValues to true. and directly passing the attribute like maxlength from parent to child.
Child Component:
<input id="firstName" #attributes="InputAttributes" />
#code {
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> InputAttributes { get; set; } =
new Dictionary<string, object>()
{
{ "placeholder", "Child Component Placeholder" }
};
}
Parent Component:
<ChildComponent InputAttributes="attributesFromParent" maxlength="15">
</ChildComponent>
#code {
public Dictionary<string, object> attributesFromParent { get; set; } =
new Dictionary<string, object>()
{
{ "required", "required" },
{ "placeholder", "Parent Component Placeholder" }
};
}
Reference: https://youtu.be/gClG243kn1o and https://www.pragimtech.com/blog/blazor/blazor-arbitrary-attributes/
Assumptions:
you want to add classes in the parent
you want to add classes in the child
you want all classes to be combined in the final markup
you want to use class... not Class, Classes, Css, CssClass or whatever!
ChildComponent.razor:
<div #attributes=_additionalAttributes></div>
#code {
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; }
= new Dictionary<string, object>();
private IReadOnlyDictionary<string, object>? _additionalAttributes;
protected override void OnParametersSet()
{
base.OnParametersSet();
var parentClasses = AdditionalAttributes.GetValueOrDefault("class", "");
var classes = $"foo {parentClasses}".Trim();
_additionalAttributes =
AdditionalAttributes.Where(x => x.Key != "class")
.Append(KeyValuePair.Create("class", (object)classes))
.ToDictionary(x => x.Key, x => x.Value);
}
}
ParentComponent.razor is now clean, with the normal name class:
<ChildComponent class="bar baz" />
#code {
}
Which renders:
<div class="foo bar baz"></div>
We can get a ModelExpression using this property in a TagHelper:
[HtmlAttributeName("asp-for")]
public ModelExpression For { get; set; }
I somehow managed* to have a ViewModel which has a ModelExpression property:
public class TemplateViewModel
{
public ModelExpression For { get; set; }
}
Every time I try to pass it, the Model expression is For from TemplateViewModel, not the real Expression which is stored into For:
#model TemplateViewModel
<input asp-for="#Model.For" class="form-control"/>
Above results in:
<input class="form-control" type="text" id="For" name="For" value="Microsoft.AspNetCore.Mvc.ViewFeatures.ModelExpression" />
I expected, the input, which is described by the ModelExpression instead of literally a ModelExpression for the ModelExpression.
*since I want to have a Template View for an TagHelper using IHtmlHelper::PartialView(). This example is heavy minimized. My main motivation is to create a single <form-group for="" /> TagHelper, which is generating a Bootstrap Form Group.
ModelExpression is handled as a special case in the Razor Compiler, so this isn't going to work directly. Since the compiler is open source, you could suggest a patch to ignore cases where the property is itself a ModelExpression. In the mean time, you're going to need to use a different type of property in your tag helper, to help you get a reference to the actual ModelExpression. Perhaps public Func<ModelExpression> ForAccessor { get; set; }?
Problem description
I'm currently working on a Wizard mechanism in our ASP.NET MVC application. However, I've faced a problem while trying to bind model in the view. In short:
I've got a wizard model which looks more or less like this:
class WizardViewModel {
public IList<StepViewModel> Steps { get; set; }
// ...
}
Each step except for last has got its own model. The last step (summary) takes whole WizardStepModel and is used only to display data (via disabled controls). Displaying values from all steps leads to this kind of code in the view:
#Html.DropDownListFor(
m => ((ConcreteStepModel)Model.Steps[0]).SelectedValue,
((ConcreteStepModel)Model.Steps[0]).SelectList,
new { disabled = "disabled" }
)
The code works, but continuous casting base step model to a concrete class only to get the value:
Is uncomfortable,
makes code less readable.
What I tried to do?
I thought that I could create an alias for each step:
#{
ConcreteStepModel stepOne = (ConcreteStepModel)Model.Steps[0];
}
And then:
#Html.DropDownListFor(
m => stepOne.SelectedValue, stepOne.SelectList, new { disabled = "disabled" }
)
It works for most of controls, but not for DropDownList. For some reason, value of the dropdown is bound incorrectly and shows first option instead of the selected one.
Question
Is there another way which I could use for creating some kind of aliases for steps from the wizard so that I don't have to perform casting each time I need to get a value? Or maybe I am doing something wrong? I'd be grateful for any help.
Since your 'last step' is just a summary and presumably used as a final 'confirmation step' before saving to the database, then you should not be generating disabled form controls for your properties. It would be rendering anywhere 2-5 times the html that is necessary which will just degrade performance.
Instead just generate the value of the property as text, for example
<div class="field">
<div class="field-label">#Html.DisplayNameFor(m => m.Steps[0].SomeProperty)</div>
<div class="field-value">#Html.DisplayFor(m => m.Steps[0].SomeProperty)</div>
</div>
and use the class names to style the layout (and the result would no doubt save a lot of screen space as well).
For the dropdownlist properties, you would need to add an additional property associated with the selected text, so in addition to public int SelectedValue { get; set; } used in the edit view, you would include an associated public string SelectedText { get; set; } property for the 'final step' view.
It also appears from your code that StepViewModel is an abstract base class and you have a concrete class for each 'step'. If that is the case, then it would be better for your WizardViewModel to contain properties for each step
public class WizardViewModel
{
public Step1Model Step1 { get; set; }
public Step2Model Step2 { get; set; }
....
}
which means if you really did want to generate form controls, it would not be be necessary to cast the type. The code would just be
#Html.DropDownListFor(m => m.Step1.SelectedValue, Model.Step1.SelectList, new { disabled = "disabled" })
The View will display either a search on Name OR Date. The problem is that the ValidationSummary (in accordance to the class) requires both values to be entered, and will add an error for the field/property that is not been displayed. Is there any way to prevent it to require the Date when the search is only prompting for the Name ? Like disabling the related Data Annotation?
NOTE: For simplicity, the code has been shortened and it is for ilustration only. Please, do not vote it down if u find a missing ";" or similar.
public class CustomSearch
{
[Required(ErrorMessage = "The text must be filled in.")]
public string SearchTextValue { get; set; }
[Required(ErrorMessage = "The Date must be selected.")]
public string SearchDateValue { get; set; }
}
<div>
<div>
#if (Model.SearchValidationSummary) { #Html.ValidationSummary(false) }
<div>
#using (Html.BeginForm("", "", FormMethod.Post ))
{
if (Model.SearchText) { #Html.TextBoxFor(m => m.SearchTextValue) }
else if (Model.SearchDate) { #Html.DateEditFor(m => m.SearchDateValue) }
}
</div>
Using something like a RequiredIf annotation, as Daniel suggests in the comments above, is probably the most elegant solution, but just for completeness, here's other options.
First, you can merely remove the error. Something along the lines of:
if (searchType = "text")
{
ModelState["SearchDateValue"].Errors.Clear();
}
if (searchType = "date")
{
ModelState["SearchTextValue"].Errors.Clear();
}
Basically, whichever type of search is done, you just clear the errors for the other field.
Second, you can manually validate instead of relying on the annotation:
if (searchType == "text" && String.IsNullOrWhiteSpace(model.SearchTextValue))
{
ModelState.AddModelError("SearchTextValue", "The text must be filled in.");
}