Blazor jquery select2 two way binding - c#

I used jquery select2 in blazor server side how i can bind selected value
<InputSelect class="form-control select2" #bind-Value="#purchaseSearch.PriorityId" id="search-priorityId">
<option value="">All</option>
#foreach (var priority in priorities)
{
<option value="#priority.Id">#priority.Name</option>
}
</InputSelect>

I has created a custom component select with select2 library for blazor.
I hope this is example for you.
- Select2.razor:
#typeparam TValue
#inherits InputBase<TValue>
#if (!string.IsNullOrWhiteSpace(Label))
{
<label class="form-control-label" for="#Id">
#Label
#if (Required)
{
<font color="red">(*)</font>
}
</label>
}
else
{
<LabelFor FieldIdentifier="FieldIdentifier"></LabelFor>
}
<select id="#Id" class="form-control select2" style="width: 100%;" >
<option #key="null" value="null">--- Chọn ---</option>
#if (Datasource != null)
#foreach (var item in Datasource)
{
if (item.Key == Value?.ToString())
{
<option #key="#item.Key" value="#item.Key" selected="selected">
#((MarkupString)#item.Value)
</option>
}
else
{
<option #key="#item.Key" value="#item.Key">
#((MarkupString)#item.Value)
</option>
}
}
</select>
<div class="form-control-validation">
<CustomValidationMessage Field="FieldIdentifier" TValue="string" />
</div>
Select2.razor.cs
public partial class SelectWithFilter<TValue> : InputBase<TValue>
{
[Parameter] public string Id { get; set; }
[Parameter] public string Label { get; set; }
[Parameter] public bool Required { get; set; }
//[Parameter] public Expression<Func<string>> ValidationFor { get; set; }
[Parameter] public ICollection<KeyValuePair<string, string>> Datasource { get; set; }
[Inject] IJSRuntime JSRuntime { get; set; }
public DotNetObjectReference<SelectWithFilter<TValue>> DotNetRef;
protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage)
{
if (value == "null")
{
value = null;
}
if (typeof(TValue) == typeof(string))
{
result = (TValue)(object)value;
validationErrorMessage = null;
return true;
}
else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(int?))
{
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue);
result = (TValue)(object)parsedValue;
validationErrorMessage = null;
return true;
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(TValue)}'.");
}
protected override void OnInitialized()
{
base.OnInitialized();
DotNetRef = DotNetObjectReference.Create(this);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("select2Component.init", Id);
await JSRuntime.InvokeVoidAsync("select2Component.onChange", Id, DotNetRef, "Change_SelectWithFilterBase");
}
}
[JSInvokable("Change_SelectWithFilterBase")]
public void Change(string value)
{
if (value == "null")
{
value = null;
}
if (typeof(TValue) == typeof(string))
{
CurrentValue = (TValue)(object)value;
}
else if (typeof(TValue) == typeof(int))
{
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue);
CurrentValue = (TValue)(object)parsedValue;
}
else if (typeof(TValue) == typeof(int?))
{
if (value == null)
{
CurrentValue = (TValue)(object)null;
}
else
{
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedValue);
CurrentValue = (TValue)(object)parsedValue;
}
}
}
}
js:
window.select2Component = {
init: function (Id) {
//Initialize Select2 Elements
$('#' + Id).select2();
},
onChange: function (id, dotnetHelper, nameFunc) {
$('#' + id).on('select2:select', function (e) {
dotnetHelper.invokeMethodAsync(nameFunc, $('#' + id).val());
});
},
}

Related

Blazor Custom Multiselect Dropdown Component Webassembly

I'm tired to search on web a solution for this. Basicly i am using InputBase to extend the normal inputbox to a custom component. For single selection in its ok, but turns complicate when i have mutiple selection "select multiple="multiple""
So here is the code:
File: XDropDownMultiSelect.razor
#using System.Linq.Expressions
#typeparam T
#inherits InputBase<T>
#if (!string.IsNullOrEmpty(Label))
{
<label class="form-label">#Label</label>
}
<select #bind="CurrentValue" class="form-control select2 #CssClass" id="#Id" #attributes="AdditionalAttributes" multiple>
#if (DropdownValues != null)
{
foreach (var cursor in DropdownValues)
{
<option value="#cursor.Key">#cursor.Value</option>
}
}
</select>
#code {
[Inject] public IJSRuntime _js { get; set; }
[Parameter, EditorRequired] public string Id { get; set; }
[Parameter] public string Label { get; set; }
[Parameter] public Expression<Func<T>> ValidationFor { get; set; }
[Parameter] public bool ShowDefaultOption { get; set; } = true;
[Parameter] public Dictionary<string, string> DropdownValues { get; set; }
[Parameter] public string Selected { get; set; }
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
{
if (typeof(T) == typeof(string))
{
result = (T)(object)value;
validationErrorMessage = null;
return true;
}
else if (typeof(T) == typeof(string[]))
{
result = (T)(object)(new string[] { value });
validationErrorMessage = null;
return true;
}
else if (typeof(T) == typeof(Guid))
{
Guid.TryParse(value, out var parsedValue);
result = (T)(object)parsedValue;
validationErrorMessage = null;
return true;
}
else if (typeof(T).IsEnum)
{
try
{
result = (T)Enum.Parse(typeof(T), value);
validationErrorMessage = null;
return true;
}
catch (ArgumentException)
{
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(T)}'.");
}
}
I use "CurrentValue" instead of "CurrentValueAsString" because is an array no string, and if i set CurrentValueAsString will have a render json problem...
Now i call by simple:
<XDropDownMultiSelect #bind-Value="usersSelected" Id="test" DropdownValues="usersAll" />
#code{
public string[] usersSelected { get; set; } = new [] { "user1" };
public string[] usersAll{ get; set; } = new [] { "user1", "user2", "user3" };
Its working, but dosen´t bind the new selection values to my selectValues object.
i found how to use CurrentValueAsString
protected override string FormatValueAsString(T? value)
{
if( value != null)
{
return value.ToJson(); //this is extension my to convert any object to json format.
}
return base.FormatValueAsString(value);
}
But this not update the source model with new selections.

how to create a generic treeview component in blazor?

could you help me? I want to make a generic tree view component in blazor webassembly but I am a bit lost in how to do it, I want to be able to pass any type of object list to the component, for the moment I have done something very simple, with an object called directory load the component but I would like to replace it with Titem to be able to send any type of list
index.razor
#page "/index"
<h1>Treeview</h1>
<Treeview Directorios="directorios"></Treeview>
#code {
public Directorio[] directorios;
protected async override Task OnInitializedAsync()
{
var fall2014 = new Directorio("Fall 2014", new Directorio[] { }, new string[] { "image1.jpg", "image2.jpg", "image3.jpg" });
var summer2014 = new Directorio("Summer 2014", new Directorio[] { }, new string[] { "image10.jpg", "image20.jpg", "image30.jpg" });
var pictures = new Directorio("Pictures", new Directorio[] { fall2014, summer2014 }, new string[] { });
var music = new Directorio("Music", new Directorio[] { }, new string[] { "song1.mp3", "song2.mp3" });
directorios = new Directorio[] { pictures, music };
}
}
component.razor
<ul>
#foreach (var dir in Directorios)
{
<li>
<span #onclick="#dir.toggle">#dir.getIcon()</span>
<span>#dir.nombre</span>
#if (dir.expanded)
{
<div>
<ul>
#foreach (var archivo in dir.archivos)
{
<li>#archivo</li>
}
</ul>
<Treeview Directorios="#dir.directorios"></Treeview>
</div>
}
</li>
}
</ul>
#code {
[Parameter] public Directorio[] Directorios { get; set; }
}
directory.cs
public class Directorio
{
public bool expanded = false;
public string nombre;
public string[] archivos;
public Directorio[] directorios;
public Directorio(string nombre, Directorio[] directorios, string[] archivos)
{
this.nombre = nombre;
this.directorios = directorios;
this.archivos = archivos;
}
public void toggle()
{
expanded = !expanded;
}
public string getIcon()
{
if (expanded)
{
return "-";
}
return "+";
}
}
Try this one.
In future perhaps I will make a combotree and share with you here.
if you are able to do same, do not hesitate to post it here.
1.Treeview.razor
#typeparam Tvalue
#inherits TreeviewBase<Tvalue>
<ul class="parentUl">
#if (AllItems != null)
{
#foreach (var Pitem in AllItems)
{
if (GetPropertyValue(Pitem, ParentId) == ""|| Convert.ToInt32(GetPropertyValue(Pitem, ParentId)) == 0)
{
if (Convert.ToBoolean(GetPropertyValue(Pitem, HasChildren)))
{
<li>
<span #onclick="#(()=>SpanToggle(Pitem))" class="#_caretcss[Convert.ToInt32(#GetPropertyValue(Pitem, Id))]">#GetPropertyValue(Pitem, Text)</span>
<ul class="#_nestedcss[Convert.ToInt32(#GetPropertyValue(Pitem, Id))]">
#foreach (var Citem in AllItems)
{
if (GetPropertyValue(Pitem, Id) == GetPropertyValue(Citem, ParentId))
{
if (Convert.ToBoolean(GetPropertyValue(Citem, HasChildren)))
{
<li>
<span #onclick="#(()=>SpanToggle(Citem))" class="#_caretcss[Convert.ToInt32(#GetPropertyValue(Citem, Id))]">#GetPropertyValue(Citem, Text)</span>
<ul class="#_nestedcss[Convert.ToInt32(#GetPropertyValue(Citem, Id))]">
#foreach (var C1item in AllItems)
{
if (GetPropertyValue(Citem, Id) == GetPropertyValue(C1item, ParentId))
{
if (Convert.ToBoolean(GetPropertyValue(C1item, HasChildren)))
{
<li>
<span #onclick="#(()=>SpanToggle(C1item))" class="#_caretcss[Convert.ToInt32(#GetPropertyValue(C1item, Id))]">#GetPropertyValue(C1item, Text)</span>
<ul class="#_nestedcss[Convert.ToInt32(#GetPropertyValue(C1item, Id))]">
#foreach (var C2item in AllItems)
{
if (GetPropertyValue(C1item, Id) == GetPropertyValue(C2item, ParentId))
{
if (Convert.ToBoolean(GetPropertyValue(C2item, HasChildren)))
{
<li>
<span #onclick="#(()=>SpanToggle(C2item))" class="#_caretcss[Convert.ToInt32(#GetPropertyValue(C2item, Id))]">#GetPropertyValue(C1item, Text)</span>
</li>
}
else
{
<li>#GetPropertyValue(C2item, Text)</li>
}
}
}
</ul>
</li>
}
else
{
<li>#GetPropertyValue(C1item, Text)</li>
}
}
}
</ul>
</li>
}
else
{
<li>#GetPropertyValue(Citem, Text)</li>
}
}
}
</ul>
</li>
}
else
{
<li>#GetPropertyValue(Pitem, Text)</li>
}
}
}
}
</ul>
2.style.css
<style type="text/css">
/*css reference W3schools. "with small modification."*/
/* css begin*/
.parentUl li ul {
border-left: dashed 2px black;
height: fit-content;
border-start-end-radius: 2px;
}
ul, .parentUl {
list-style-type: none;
}
.parentUl ul li {
position: relative;
}
.parentUl ul li:before {
content: "";
position: absolute;
top: 13px;
left: -40px;
width: 40px;
height: 1px;
border-bottom: dashed 2px black;
}
.parentUl {
margin: 0;
padding: 0;
}
.caret {
cursor: pointer;
-webkit-user-select: none; /* Safari 3.1+ */
-moz-user-select: none; /* Firefox 2+ */
-ms-user-select: none; /* IE 10+ */
user-select: none;
}
.caret::before {
content: "\25B6";
color: black;
display: inline-block;
margin-right: 6px;
transition: all 0.45s;
}
.caret-down::before {
-ms-transform: rotate(60deg); /* IE 9 */
-webkit-transform: rotate(60deg); /* Safari */
transform: rotate(60deg);
transition: all 0.45s;
}
.nested {
display: none;
transition: all 0.45s;
}
.active {
display: block;
transition: all 0.45s;
}
/*css end*/
</style>
3.TreeviewBase.cs
public partial class TreeviewBase<Tvalue>:ComponentBase
{
[Parameter]
public List<Tvalue> DataSource { get; set; }
[Parameter]
public string Id { get; set; }
[Parameter]
public string ParentId { get; set; }
[Parameter]
public string HasChildren { get; set; }
[Parameter]
public string Text { get; set; }
[Parameter]
public string Expanded { get; set; }
protected List<Tvalue> AllItems;
protected Dictionary<int, bool> _caretDown= new Dictionary<int, bool>();
protected Dictionary<int, string> _caretcss=new Dictionary<int,string>();
protected Dictionary<int, string> _nestedcss=new Dictionary<int,string>();
protected override Task OnInitializedAsync()
{
//asigning to its new instance to avoid exceptions.
AllItems = new List<Tvalue>();
AllItems = DataSource.ToArray().ToList();
if (AllItems != null)
{
foreach (var item in AllItems)
{
var _id = Convert.ToInt32(GetPropertyValue(item, Id));
//initializing fields with default value.
_caretDown.Add(_id, true);
_caretcss.Add(_id, "caret");
_nestedcss.Add(_id, "nested");
}
}
return base.OnInitializedAsync();
}
protected override Task OnParametersSetAsync()
{
//This will check if the first item in the
// list/collection has a "parentId" then remove the "parentId" from it.
//Because we use the first item as a reference in the razor file, so it must not have "parentId".
var Parem = AllItems.First();
switch (GetPropertyType(Parem, ParentId))
{
case "Int32":
if (Convert.ToInt32(GetPropertyValue(Parem, ParentId)) > 0)
{
SetPropertyValue<int>(Parem, ParentId, 0);
}
break;
case "String":
if (GetPropertyValue(Parem, ParentId) != "")
{
SetPropertyValue<string>(Parem, ParentId, "");
}
break;
default:
break;
}
return base.OnParametersSetAsync();
}
protected void SpanToggle(Tvalue item)
{
var _clckdItemid = Convert.ToInt32(GetPropertyValue(item, Id));
_caretcss[_clckdItemid] = _caretDown[_clckdItemid] ? "caret caret-down" : "caret";
_nestedcss[_clckdItemid] = _caretDown[_clckdItemid] ? "active" : "nested";
_caretDown[_clckdItemid] = !_caretDown[_clckdItemid];
}
#region reflection methodes to get your property type, propert value and also set property value
protected string GetPropertyValue(Tvalue item, string Property)
{
if (item != null)
{
return item.GetType().GetProperty(Property).GetValue(item, null).ToString();
}
return "";
}
protected void SetPropertyValue<T>(Tvalue item, string Property, T value)
{
if (item != null)
{
item.GetType().GetProperty(Property).SetValue(item, value);
}
}
protected string GetPropertyType(Tvalue item, string Property)
{
if (item != null)
{
return item.GetType().GetProperty(Property).PropertyType.Name;
}
return null;
}
#endregion
}
Index.razor
<Treeview Tvalue="MailItem" DataSource="#MyFolder" Id="Id" Text="FolderName" ParentId="ParentId"
Expanded="Expanded" HasChildren="HasSubFolder"></Treeview>
#code{
protected class MailItem
{
public int Id { get; set; }
public string ParentId { get; set; }
public bool HasSubFolder { get; set; }
public string FolderName { get; set; }
public bool Expanded { get; set; }
}
List<MailItem> MyFolder = new List<MailItem>();
protected override Task OnInitializedAsync()
{
MyFolder.Add(new MailItem { Id = 1, FolderName = "Inbox", HasSubFolder = true, Expanded = true, ParentId = "" });
MyFolder.Add(new MailItem { Id = 2, FolderName = "Category", ParentId = "1", HasSubFolder = true, Expanded = true });
MyFolder.Add(new MailItem { Id = 3, FolderName = "Primary", ParentId = "2", HasSubFolder = false, Expanded = true });
MyFolder.Add(new MailItem { Id = 4, FolderName = "Social", ParentId = "6", HasSubFolder = false, Expanded = true });
MyFolder.Add(new MailItem { Id = 5, FolderName = "Promotion", ParentId = "6", HasSubFolder = false, Expanded = true });
MyFolder.Add(new MailItem { Id = 6, FolderName = "Demo", ParentId = "2", HasSubFolder = true, Expanded = true });
return base.OnInitializedAsync();
}
}
This is the logics behind a generic Treeview Component in Blazor.
Kindly click on this image to view the working example:

Assign the value of a property based on another property in model ASP.NET Core MVC 5

What I want to do, as the title says, is assign a value to a property of the model, based on on another property.
In my specific case, what I want to do is assign the property blongmonth (which is a string, for example "January") to bmonth (which is the number of the month, in this case would be 1, and is an integer). I have to work with this types, since I am working on a table which I cannot modify.
The problem is, the harder I try, I cannot find a working solution, and online I don't find nothing specific which helps me solving my little problem. When, in my app, I select (from a dropdown menu) the month (blongmonth), the number of the month (bmonth) in the database is always "0".
I've tried creating a method in the ViewModel and calling it in the Model (not a good idea) but doesn't work, as the ViewModel is only to present data. Now I'm trying to do the thing directly in the model:
public class Bdgfixmonth
{
[Key]
public int Counter { get; set; }
[Required]
public int Byear { get; set; }
[Required]
public string Bbudget { get; set; }
[Required]
//public int Bmonth { get; set; }
private int _Bmonth;
public int Bmonth
{
get
{
return _Bmonth;
}
set
{
if (Blongmonth == "January")
{
_Bmonth = 1;
}
else if (Blongmonth == "February")
{
_Bmonth = 2;
}
else if (Blongmonth == "March")
{
_Bmonth = 3;
}
else if (Blongmonth == "April")
{
_Bmonth = 4;
}
else if (Blongmonth == "May")
{
_Bmonth = 5;
}
else if (Blongmonth == "June")
{
_Bmonth = 6;
}
else if (Blongmonth == "July")
{
_Bmonth = 7;
}
else if (Blongmonth == "August")
{
_Bmonth = 8;
}
else if (Blongmonth == "September")
{
_Bmonth = 9;
}
else if (Blongmonth == "October")
{
_Bmonth = 10;
}
else if (Blongmonth == "November")
{
_Bmonth = 11;
}
else if (Blongmonth == "December")
{
_Bmonth = 12;
}
else
{
_Bmonth = 0;
}
}
}
[Required]
public string Blongmonth
{ get; set; }
[Required]
public int Closed { get; set; }
[Required]
public string Current { get; set; }
}
I tried also putting this code in the getter of Blongmonth, creating also a private variable, but it doesn't work. Also tried this way:
if (Blongmonth == "January")
{
_Bmonth = 1;
_Bmonth = Bmonth;
}
This is the dropdown menu in the Create View:
<div class="form-group">
<label asp-for="Blongmonth" class="control-label">#ViewBag.D</label>
<select asp-for="Blongmonth" class="form-control" asp-action="">
<option value="January">January</option>
<option value="February">February</option>
<option value="March">March</option>
<option value="April">April</option>
<option value="May">May</option>
<option value="June">June</option>
<option value="July">July</option>
<option value="August">August</option>
<option value="September">September</option>
<option value="October">October</option>
<option value="November">November</option>
<option value="December">December</option>
</select>
<span asp-validation-for="Blongmonth" class="text-danger"></span>
</div>
I also tried creating a trigger directly in SQL Server but it doesn't work:
SELECT
bmonth,
CASE
WHEN blongmonth = 'January' THEN 1
ELSE 'False'
END
FROM bdgfixmonth
If I choose January in this test, it will go to "false".
What am I doing wrong? Maybe I have to to this in the controller? I am a humble beginner, I would appreciate if someone more experienced than me could help me.
You shouldn't need the backing _Bmonth variable if the value is entirely determined by another field. What you have in your setter code, move it to the get and just return should work. Also, no need for a set if you never assign to it:
public int Bmonth
{
get
{
if (Blongmonth == "January")
{
return 1;
}
else if (Blongmonth == "February")
{
return 2;
}
else if (Blongmonth == "March")
{
return 3;
}
else if (Blongmonth == "April")
{
return 4;
}
else if (Blongmonth == "May")
{
return 5;
}
else if (Blongmonth == "June")
{
return 6;
}
else if (Blongmonth == "July")
{
return 7;
}
else if (Blongmonth == "August")
{
return 8;
}
else if (Blongmonth == "September")
{
return 9;
}
else if (Blongmonth == "October")
{
return 10;
}
else if (Blongmonth == "November")
{
return 11;
}
else if (Blongmonth == "December")
{
return 12;
}
else
{
return 0;
}
}
}

Blazor pass ValidationMessage to extended InputText component

I have an ExtendedInputText component which inherits from InputText
#inherits InputText
<div class="flex">
<label class="w-1/2">
#Label
#if(Required){
<span class="text-red-500 ml-1">*</span>
}
</label>
<InputText
class="flex-1 border border-gray-200 bg-white p-2 rounded"
placeholder="#Label"
Value="#Value"
ValueChanged="#ValueChanged"
ValueExpression="#ValueExpression"
Required="#Required"
/>
</div>
#code
{
[Parameter]
public bool Required { get; set; }
[Parameter]
public string Label { get; set; }
}
I intend on using it to replace this
<EditForm Model="Command" OnValidSubmit="OnValidSubmit">
<FluentValidationValidator />
<ValidationSummary />
<div class="">
<label>Title <span class="text-red-500">*</span></label>
<InputText id="Title" #bind-Value="Command.Title" />
<ValidationMessage For="#(() => Command.Title)" />
</div>
<button type="submit" class="p-2 bg-positive-500 text-white rounded">Create</button>
</EditForm>
with this
<EditForm Model="Command" OnValidSubmit="OnValidSubmit">
<FluentValidationValidator />
<ValidationSummary />
<ExtendedInputText Label="Title" Required="true" #bind-Value="Command.Title"/>
<button type="submit" class="p-2 bg-positive-500 text-white rounded">Create</button>
</EditForm>
How would I go about also passing <ValidationMessage For="#(() => Command.Title)" /> to the ExtendedInputText component and rendering it from within?
With the help of Nicola and Shaun, this is the solution that worked for me.
#inherits InputText
<div class="flex">
<label class="w-1/2 text-right font-semibold mr-1 py-2">
#Label
#if (Required)
{
<span class="text-red-500 ml-1">*</span>
}
</label>
<div class="flex-1">
<InputText class="w-full border border-gray-200 bg-white p-2 rounded"
placeholder="#Label"
Value="#Value"
ValueChanged="#ValueChanged"
ValueExpression="#ValueExpression"
Required="#Required"/>
#ValidationFragment
</div>
</div>
#code
{
[Parameter]
public bool Required { get; set; }
[Parameter]
public string Label { get; set; }
private RenderFragment ValidationFragment => (builder) =>
{
var messages = EditContext.GetValidationMessages(FieldIdentifier).ToList();
if(messages is not null && messages.Count > 0)
{
builder.OpenElement(310, "div");
builder.AddAttribute(320, "class", "text-red-500 p-2 w-full");
builder.OpenComponent<ValidationMessage<string>>(330);
builder.AddAttribute(340, "For", ValueExpression);
builder.CloseComponent();
builder.CloseElement();
}
};
}
They key part was the private RenderFragment ValidationFragment which is built programatically to display associated errors stored in the cascading EditContext
The full code for a similar component. Note that the component gets the Validation message, you don't need to pass it.
I think I've included the only dependant class, but I may have missed something.
/// ============================================================
/// Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// ============================================================
using Blazr.SPA.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Linq;
using System.Linq.Expressions;
#nullable enable
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
#pragma warning disable CS8602 // Dereference of a possibly null reference.
namespace Blazr.UIComponents
{
public class FormEditControl<TValue> : ComponentBase
{
[Parameter]
public TValue? Value { get; set; }
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
[Parameter] public string? Label { get; set; }
[Parameter] public string? HelperText { get; set; }
[Parameter] public string DivCssClass { get; set; } = "mb-2";
[Parameter] public string LabelCssClass { get; set; } = "form-label";
[Parameter] public string ControlCssClass { get; set; } = "form-control";
[Parameter] public Type ControlType { get; set; } = typeof(InputText);
[Parameter] public bool ShowValidation { get; set; }
[Parameter] public bool ShowLabel { get; set; } = true;
[Parameter] public bool IsRequired { get; set; }
[Parameter] public bool IsRow { get; set; }
[CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;
private readonly string formId = Guid.NewGuid().ToString();
private bool IsLabel => this.ShowLabel && (!string.IsNullOrWhiteSpace(this.Label) || !string.IsNullOrWhiteSpace(this.FieldName));
private bool IsValid;
private FieldIdentifier _fieldIdentifier;
private ValidationMessageStore? _messageStore;
private string? DisplayLabel => this.Label ?? this.FieldName;
private string? FieldName
{
get
{
string? fieldName = null;
if (this.ValueExpression != null)
ParseAccessor(this.ValueExpression, out var model, out fieldName);
return fieldName;
}
}
private string MessageCss => CSSBuilder.Class()
.AddClass("invalid-feedback", !this.IsValid)
.AddClass("valid-feedback", this.IsValid)
.Build();
private string ControlCss => CSSBuilder.Class(this.ControlCssClass)
.AddClass("is-valid", this.IsValid)
.AddClass("is-invalid", !this.IsValid)
.Build();
protected override void OnInitialized()
{
if (CurrentEditContext is null)
throw new InvalidOperationException($"No Cascading Edit Context Found!");
if (ValueExpression is null)
throw new InvalidOperationException($"No ValueExpression defined for the Control! Define a Bind-Value.");
if (!ValueChanged.HasDelegate)
throw new InvalidOperationException($"No ValueChanged defined for the Control! Define a Bind-Value.");
CurrentEditContext.OnFieldChanged += FieldChanged;
CurrentEditContext.OnValidationStateChanged += ValidationStateChanged;
_messageStore = new ValidationMessageStore(this.CurrentEditContext);
_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
if (_messageStore is null)
throw new InvalidOperationException($"Cannot set the Validation Message Store!");
var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
var showHelpText = (messages.Count == 0) && this.IsRequired && this.Value is null;
if (showHelpText && !string.IsNullOrWhiteSpace(this.HelperText))
_messageStore.Add(_fieldIdentifier, this.HelperText);
}
protected void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
{
var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
if (messages != null || messages.Count > 1)
{
_messageStore.Clear();
}
}
protected void FieldChanged(object sender, FieldChangedEventArgs e)
{
if (e.FieldIdentifier.Equals(_fieldIdentifier))
_messageStore.Clear();
}
protected override void OnParametersSet()
{
this.IsValid = true;
{
if (this.IsRequired)
{
this.IsValid = false;
var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
if (messages is null || messages.Count == 0)
this.IsValid = true;
}
}
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (IsRow)
builder.AddContent(1, RowFragment);
else
builder.AddContent(2, BaseFragment);
}
private RenderFragment BaseFragment => (builder) =>
{
builder.OpenElement(0, "div");
builder.AddAttribute(10, "class", this.DivCssClass);
builder.AddContent(40, this.LabelFragment);
builder.AddContent(60, this.ControlFragment);
builder.AddContent(70, this.ValidationFragment);
builder.CloseElement();
};
private RenderFragment RowFragment => (builder) =>
{
builder.OpenElement(0, "div");
builder.AddAttribute(10, "class", "row form-group");
builder.OpenElement(20, "div");
builder.AddAttribute(30, "class", "col-12 col-md-3");
builder.AddContent(40, this.LabelFragment);
builder.CloseElement();
builder.OpenElement(40, "div");
builder.AddAttribute(50, "class", "col-12 col-md-9");
builder.AddContent(60, this.ControlFragment);
builder.AddContent(70, this.ValidationFragment);
builder.CloseElement();
builder.CloseElement();
};
private RenderFragment LabelFragment => (builder) =>
{
if (this.IsLabel)
{
builder.OpenElement(110, "label");
builder.AddAttribute(120, "for", this.formId);
builder.AddAttribute(130, "class", this.LabelCssClass);
builder.AddContent(140, this.DisplayLabel);
builder.CloseElement();
}
};
private RenderFragment ControlFragment => (builder) =>
{
builder.OpenComponent(210, this.ControlType);
builder.AddAttribute(220, "class", this.ControlCss);
builder.AddAttribute(230, "Value", this.Value);
builder.AddAttribute(240, "ValueChanged", EventCallback.Factory.Create(this, this.ValueChanged));
builder.AddAttribute(250, "ValueExpression", this.ValueExpression);
builder.CloseComponent();
};
private RenderFragment ValidationFragment => (builder) =>
{
if (this.ShowValidation && !this.IsValid)
{
builder.OpenElement(310, "div");
builder.AddAttribute(320, "class", MessageCss);
builder.OpenComponent<ValidationMessage<TValue>>(330);
builder.AddAttribute(340, "For", this.ValueExpression);
builder.CloseComponent();
builder.CloseElement();
}
else if (!string.IsNullOrWhiteSpace(this.HelperText))
{
builder.OpenElement(350, "div");
builder.AddAttribute(360, "class", MessageCss);
builder.AddContent(370, this.HelperText);
builder.CloseElement();
}
};
// Code lifted from FieldIdentifier.cs
private static void ParseAccessor<T>(Expression<Func<T>> accessor, out object model, out string fieldName)
{
var accessorBody = accessor.Body;
if (accessorBody is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert && unaryExpression.Type == typeof(object))
accessorBody = unaryExpression.Operand;
if (!(accessorBody is MemberExpression memberExpression))
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
fieldName = memberExpression.Member.Name;
if (memberExpression.Expression is ConstantExpression constantExpression)
{
if (constantExpression.Value is null)
throw new ArgumentException("The provided expression must evaluate to a non-null value.");
model = constantExpression.Value;
}
else if (memberExpression.Expression != null)
{
var modelLambda = Expression.Lambda(memberExpression.Expression);
var modelLambdaCompiled = (Func<object?>)modelLambda.Compile();
var result = modelLambdaCompiled();
if (result is null)
throw new ArgumentException("The provided expression must evaluate to a non-null value.");
model = result;
}
else
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
}
}
}
#pragma warning restore CS8622
#pragma warning restore CS8602
#nullable disable
and CSSBuilder
/// ============================================================
/// Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// ============================================================
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace Blazr.SPA.Components
{
public class CSSBuilder
{
private Queue<string> _cssQueue = new Queue<string>();
public static CSSBuilder Class(string cssFragment = null)
{
var builder = new CSSBuilder(cssFragment);
return builder.AddClass(cssFragment);
}
public CSSBuilder()
{
}
public CSSBuilder (string cssFragment)
{
AddClass(cssFragment);
}
public CSSBuilder AddClass(string cssFragment)
{
if (!string.IsNullOrWhiteSpace(cssFragment)) _cssQueue.Enqueue(cssFragment);
return this;
}
public CSSBuilder AddClass(IEnumerable<string> cssFragments)
{
if (cssFragments != null)
cssFragments.ToList().ForEach(item => _cssQueue.Enqueue(item));
return this;
}
public CSSBuilder AddClass(string cssFragment, bool WhenTrue)
{
if (WhenTrue) return this.AddClass(cssFragment);
return this;
}
public CSSBuilder AddClassFromAttributes(IReadOnlyDictionary<string, object> additionalAttributes)
{
if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
_cssQueue.Enqueue(val.ToString());
return this;
}
public CSSBuilder AddClassFromAttributes(IDictionary<string, object> additionalAttributes)
{
if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
_cssQueue.Enqueue(val.ToString());
return this;
}
public string Build(string CssFragment = null)
{
if (!string.IsNullOrWhiteSpace(CssFragment)) _cssQueue.Enqueue(CssFragment);
if (_cssQueue.Count == 0)
return string.Empty;
var sb = new StringBuilder();
foreach(var str in _cssQueue)
{
if (!string.IsNullOrWhiteSpace(str)) sb.Append($" {str}");
}
return sb.ToString().Trim();
}
}
}
It looks like this in action:
I use the following code for a component I've created LabelText but should be used for your case:
public partial class LabelText<T>: ComponentBase
{
[Parameter] public Expression<Func<T>> For { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
private FieldIdentifier _fieldIdentifier;
...
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "label");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "for", _fieldIdentifier.FieldName);
builder.AddContent(3, label + GetRequired());
builder.CloseElement();
}
protected override void OnParametersSet()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{GetType()} requires a cascading parameter " +
$"of type {nameof(EditContext)}. For example, you can use {GetType()} inside " +
$"an {nameof(EditForm)}.");
}
if (For == null) // Not possible except if you manually specify T
{
throw new InvalidOperationException($"{GetType()} requires a value for the " +
$"{nameof(For)} parameter.");
}
_fieldIdentifier = FieldIdentifier.Create(For);
}
UPDATE
I can't explain better then the excellent piece of code from #MrC
private RenderFragment ValidationFragment => (builder) =>
{
if (this.ShowValidation && !this.IsValid)
{
builder.OpenElement(310, "div");
builder.AddAttribute(320, "class", MessageCss);
builder.OpenComponent<ValidationMessage<TValue>>(330);
builder.AddAttribute(340, "For", this.ValueExpression);
builder.CloseComponent();
builder.CloseElement();
}
else if (!string.IsNullOrWhiteSpace(this.HelperText))
{
builder.OpenElement(350, "div");
builder.AddAttribute(360, "class", MessageCss);
builder.AddContent(370, this.HelperText);
builder.CloseElement();
}
};
you only need to add a paremeter like ValidationMessage="(() => Command.Title)" and this RenderFragment does the job for you.

Custom Blazor DateTime using inherits InputBase<DateTime> with ValidationMessage

I have the following custom date input:
CustomDateInput.razor
<input type="date"
class="#CssClass"
#bind=CurrentValue
#attributes=AdditionalAttributes />
#if (ValidationFor is not null)
{
<ValidationMessage For="#ValidationFor" />
}
#using System.Linq.Expressions
#inherits InputBase<DateTime?>
#code {
[Parameter] public Expression<Func<DateTime>>? ValidationFor { get; set; }
protected override bool TryParseValueFromString(string? value, out DateTime? result, out string validationErrorMessage)
{
result = CurrentValue;
validationErrorMessage = "";
return true;
}
}
FooPage.razor
<CustomInputDate #bind-Value="FooDate" ValidationFor="() => FooDate"></CustomInputDate>
FooPage.razor.cs
public DateTime? FooDate { get; set; }
However, I get errors:
How can I modify my CustomDateInput to allow for a Validation parameter to show a ValidationMessage?
The component. I've lifted the BuildRenderTree code from InputDate and added in the ValidationMessage component. You need to do it this way as I don't know a way to do the For binding in Razor. I've tied the For directly into the ValueExpression property of InputBase. You'll probably need to add a bit of formatting/css to prettify it.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
using System;
namespace Blazor.Starter.Components.TestComponents
{
public class CustomDate : InputDate<DateTime?>
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "type", "date");
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
builder.CloseElement();
builder.OpenComponent<ValidationMessage<DateTime?>>(6);
builder.AddAttribute(7, "For", this.ValueExpression);
builder.CloseComponent();
}
}
}
And a demo page. There's a button to manually set an error message in the validationMessageStore.
#page "/editortest"
<h3>EditorTest</h3>
<EditForm EditContext="editContext">
<div>
<CustomDate #bind-Value="model.Date"></CustomDate>
</div>
</EditForm>
<div class="m-3 p-3"><input #bind-value="_errormessage"><button class="btn btn-dark ms-2" #onclick="SetError">Set Error</button></div>
#code {
private dataModel model { get; set; } = new dataModel();
private EditContext editContext;
private string _errormessage { get; set; } = "Error in date";
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(model);
return base.OnInitializedAsync();
}
private void SetError( MouseEventArgs e)
{
var validationMessageStore = new ValidationMessageStore(this.editContext);
validationMessageStore.Clear();
var fi = new FieldIdentifier(this.model, "Date");
validationMessageStore.Add(fi, _errormessage);
}
public class dataModel
{
public string Email { get; set; }
public DateTime? Date { get; set; }
}
}
I have different way to reach the same result, you can try it!:
[Parameter] public bool ShowError { get; set; } = false;
<input type="date"
class="#CssClass"
#bind=CurrentValue
#attributes=AdditionalAttributes #onfocus="#(() => ShowError =true)"/>
#if(ShoError)
{
foreach(var msg in EditCotext.GetValidationMessages(FieldIdententifier))
{
<div class="alidation-message">#msg</div>
}
}
you need aslo to keep you code TryParseValueFromString

Categories

Resources