blazor 如何将某种类型的列表传递给 EditorAttribute
blazor how pass list of some type to EditorAttribute
我正在构建一些通用表单生成器
所以我现在可以
public class Model
{
[Editor(typeof(CustomIntEditor), typeof(InputBase<>))]
public int? testInt{ get; set; }
}
所以CustomIntEditor.razor
@using System.Diagnostics.CodeAnalysis
@using Microsoft.AspNetCore.Components.Forms
@inherits InputBase<int?>
<select @attributes="AdditionalAttributes"
type="number"
class="@CssClass"
value="@CurrentValueAsString"
@onchange="e => CurrentValueAsString = (string?)e.Value">
<option value =1>Choice 1</option>
<option value =2>Choice 2</option>
<option value =3 >Choice 3</option>
@code {
protected override string FormatValueAsString(int? value)
{
if (value != null) return value.ToString()!; else return string.Empty;
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out int? result, [NotNullWhen(false)] out string? validationErrorMessage)
{
validationErrorMessage = null;
if (value != null) result = int.Parse(value!); else result = null;
return true;
}
}
所以问题是这个编辑器如何传递一些 List<KeyValuePair<int,string>>
所以我可以构建这个选项/值,例如
List<KeyValuePair<int,string>> L = new List<KeyValuePair<int,string>>(){
{1,"asd"},
{2,"bsd"}
}
.
.
.
[Editor(typeof(CustomIntEditor), typeof(InputBase<>),L)]
public int? testInt{ get; set; }
并循环构建列表,如
<option value =@key>@value</option>
------------更新------------
我现在就是这样
private RenderFragment CreateOptionsListComponent() => builder =>
{
var optionsListAttribute = (OptionsListAttribute?)property.GetCustomAttributes(typeof(OptionsListAttribute), false).FirstOrDefault();
if (optionsListAttribute is not null)
{
var optionsList = (SortedDictionary<int, string>?)typeof(TModel).GetProperty(optionsListAttribute.List, typeof(SortedDictionary<int, string>))?.GetValue(model!);
}
builder.OpenComponent(0,typeof(InputOptionsListSelect<>).MakeGenericType(property!.PropertyType));
builder.AddAttribute(1, "Value", Value);
builder.AddAttribute(2, "ValueChanged", changeHandler);
builder.AddAttribute(3, "ValueExpression", lambdaExpression);
builder.AddAttribute(4, "id", FieldId());
builder.AddAttribute(5, "class", "form-control");
builder.CloseComponent();
}
但是如何将此 optionsList 传递给此 InputOptionsListSelect<> ?我不能自己实例化这个组件?据我检查,它需要无参数的构造函数。有什么想法吗?
我试过这样
builder.AddContent(6,ListOptions);
和
private RenderFragment ListOptions => (__builder) =>
{
foreach(var option in this.optionsList!)
{
__builder.OpenElement(7, "option");
__builder.AddAttribute(8, "value", option.Key);
__builder.AddContent(9, option.Value);
__builder.CloseElement();
}
};
但它什么也没做。不知道它放在哪里;P
--------------更新2----------------
是的,我明白了,但这仍然解决了我的问题
我有一个适合你的地方是对的,但他仍然没有解决它。我没有地方放
请检查
和
看看他是怎么用的
InputEnumSelect - 这是我需要做的,但这不是枚举;P
这是我的概念,并在此基础上做了一些修改/添加了一些功能等
所以任何组件中都没有 .razor - 代码中的所有内容
并且初始组件使用是
<DynamicFormComponent
TModel=DataX
Model=d
OnValidSubmitCallback="OnValidSubmit"
ShowValidationSummary=true
ShowValidationUnderField=true>
我发现我可能可以通过 AdditionalAtributes 做到这一点,因为它是 string/object 对 - 不确定它是否是存储列表的最佳位置,但我相信可以通过这种方式完成
感谢和问候!
我不知道您是如何生成表单控件或进行绑定的,但这里介绍了如何执行属性位。我已将其连接成标准形式,以便您可以查看实际代码。
@page "/"
<h3>GenericForm</h3>
<EditForm EditContext=this.editContext>
<InputSelect class="form-select" @bind-Value=modelData.Id>
@this.ListOptions
</InputSelect>
</EditForm>
@code {
public TestModel modelData = new TestModel() { Id = 2 };
private EditContext? editContext;
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(modelData);
return base.OnInitializedAsync();
}
private SortedDictionary<int, string> GetFieldList(string fieldName)
{
var list = new SortedDictionary<int, string>();
var typeInfo = this.modelData.GetType();
var prop = typeInfo.GetProperty(fieldName);
var editorAttr = prop?.GetCustomAttributes(true).ToList().SingleOrDefault(item => item is OptionListAttribute);
if (editorAttr is not null)
{
OptionListAttribute attr = (OptionListAttribute)editorAttr;
var obj = typeInfo.GetProperty(attr.List)?.GetValue(modelData);
if (obj is not null)
list = (SortedDictionary<int, string>)obj;
}
return list;
}
private RenderFragment ListOptions => (__builder) =>
{
@foreach (var option in this.GetFieldList("Id"))
{
<option value="@option.Key">@option.Value</option>
}
};
public class TestModel
{
[OptionList("LookupList")]
public int Id { get; set; }
public SortedDictionary<int, string> LookupList { get; set; } = new SortedDictionary<int, string>()
{
{ 1, "UK" },
{ 2, "France" },
{ 3, "Spain" },
};
}
[AttributeUsage(AttributeTargets.Property)]
public class OptionListAttribute : Attribute
{
public string List { get; set; }
public OptionListAttribute(string list)
{
List = list;
}
}
}
更新
根据更新后的问题,这里有一个自定义组件,显示如何从 ValueExpression
获取模型信息和属性值并构建选项列表。
@using System.Linq.Expressions
@typeparam TValue
<InputSelect @attributes=UserAttributes Value="@this.Value" ValueChanged=this.ValueChanged ValueExpression=this.ValueExpression!>
@this.ListOptions
</InputSelect>
@code {
[Parameter] public TValue? Value { get; set; }
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
private string? fieldName;
private object? model;
private SortedDictionary<TValue, string>? optionList;
protected override void OnInitialized()
{
base.OnInitialized();
if (this.ValueExpression is null)
throw new NullReferenceException("You must set a ValueExpression for the component");
// As we get the ValueExpression we can use it to get the property name and the model
ParseAccessor<TValue>(this.ValueExpression, out model, out fieldName);
// And then get the OptionList
GetOptionList();
}
private void GetOptionList()
{
optionList = new SortedDictionary<TValue, string>();
var typeInfo = this.model?.GetType();
if (typeInfo is not null)
{
var prop = typeInfo.GetProperty(fieldName!);
var editorAttr = prop?.GetCustomAttributes(true).ToList().SingleOrDefault(item => item is OptionListAttribute);
if (editorAttr is not null)
{
OptionListAttribute attr = (OptionListAttribute)editorAttr;
var obj = typeInfo.GetProperty(attr.List)?.GetValue(model);
if (obj is null)
throw new ArgumentException("The provided field must implement the OptionList Attribute.");
optionList = (SortedDictionary<TValue, string>)obj;
}
}
}
// This method takes the Expression provided in ValueExpression and gets the model object and the name of the field referenced
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.");
}
private RenderFragment ListOptions => (__builder) =>
{
@if (this.optionList is not null)
{
@foreach (var option in this.optionList)
{
<option value="@option.Key.ToString()">@option.Value</option>
}
}
};
}
以及修改后的演示页面:
@page "/"
<h3>GenericForm</h3>
<EditForm EditContext=this.editContext>
<div class="p-2">
<MySelect class="form-select" @bind-Value=modelData.Id />
</div>
</EditForm>
@code {
public TestModel modelData = new TestModel() { Id = 2 };
private EditContext? editContext;
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(modelData);
return base.OnInitializedAsync();
}
public class TestModel
{
[OptionList("LookupList")]
public int Id { get; set; }
public SortedDictionary<int, string> LookupList { get; set; } = new SortedDictionary<int, string>()
{
{ 1, "UK" },
{ 2, "France" },
{ 3, "Spain" },
};
}
}
第二次更新
这是在 InputEnumSelect
版本中实现的上面的代码
public sealed class InputListSelect<TValue> : InputBase<TValue>
{
private string? fieldName;
private object? model;
#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
private SortedDictionary<TValue, string>? optionList;
#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
protected override void OnInitialized()
{
base.OnInitialized();
if (this.ValueExpression is null)
throw new NullReferenceException("You must set a ValueExpression for the component");
// As we get the ValueExpression we can use it to get the property name and the model
ParseAccessor<TValue>(this.ValueExpression, out model, out fieldName);
// And then get the OptionList
GetOptionList();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "select");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string?>(this, value => CurrentValueAsString = value, CurrentValueAsString, culture: null));
if (optionList is not null)
{
foreach (var option in optionList)
{
builder.OpenElement(5, "option");
builder.AddAttribute(6, "value", option.Key?.ToString());
builder.AddContent(7, option.Value);
builder.CloseElement();
}
}
builder.CloseElement(); // close the select element
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
// Let's Blazor convert the value for us
if (BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out TValue? parsedValue))
{
result = parsedValue!;
validationErrorMessage = "";
return true;
}
// Map null/empty value to null if the bound object is nullable
if (string.IsNullOrEmpty(value))
{
var nullableType = Nullable.GetUnderlyingType(typeof(TValue));
if (nullableType != null)
{
result = default!;
validationErrorMessage = "";
return true;
}
}
// The value is invalid => set the error message
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
private void GetOptionList()
{
#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
optionList = new SortedDictionary<TValue, string>();
var typeInfo = this.model?.GetType();
if (typeInfo is not null)
{
var prop = typeInfo.GetProperty(fieldName!);
var editorAttr = prop?.GetCustomAttributes(true).ToList().SingleOrDefault(item => item is OptionListAttribute);
if (editorAttr is not null)
{
OptionListAttribute attr = (OptionListAttribute)editorAttr;
var obj = typeInfo.GetProperty(attr.List)?.GetValue(model);
if (obj is null)
throw new ArgumentException("The provided field must implement the OptionList Attribute.");
optionList = (SortedDictionary<TValue, string>)obj;
}
}
#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
}
// This method takes the Expression provided in ValueExpression and gets the model object and the name of the field referenced
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.");
}
}
我正在构建一些通用表单生成器 所以我现在可以
public class Model
{
[Editor(typeof(CustomIntEditor), typeof(InputBase<>))]
public int? testInt{ get; set; }
}
所以CustomIntEditor.razor
@using System.Diagnostics.CodeAnalysis
@using Microsoft.AspNetCore.Components.Forms
@inherits InputBase<int?>
<select @attributes="AdditionalAttributes"
type="number"
class="@CssClass"
value="@CurrentValueAsString"
@onchange="e => CurrentValueAsString = (string?)e.Value">
<option value =1>Choice 1</option>
<option value =2>Choice 2</option>
<option value =3 >Choice 3</option>
@code {
protected override string FormatValueAsString(int? value)
{
if (value != null) return value.ToString()!; else return string.Empty;
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out int? result, [NotNullWhen(false)] out string? validationErrorMessage)
{
validationErrorMessage = null;
if (value != null) result = int.Parse(value!); else result = null;
return true;
}
}
所以问题是这个编辑器如何传递一些 List<KeyValuePair<int,string>>
所以我可以构建这个选项/值,例如
List<KeyValuePair<int,string>> L = new List<KeyValuePair<int,string>>(){
{1,"asd"},
{2,"bsd"}
}
.
.
.
[Editor(typeof(CustomIntEditor), typeof(InputBase<>),L)]
public int? testInt{ get; set; }
并循环构建列表,如
<option value =@key>@value</option>
------------更新------------
我现在就是这样
private RenderFragment CreateOptionsListComponent() => builder =>
{
var optionsListAttribute = (OptionsListAttribute?)property.GetCustomAttributes(typeof(OptionsListAttribute), false).FirstOrDefault();
if (optionsListAttribute is not null)
{
var optionsList = (SortedDictionary<int, string>?)typeof(TModel).GetProperty(optionsListAttribute.List, typeof(SortedDictionary<int, string>))?.GetValue(model!);
}
builder.OpenComponent(0,typeof(InputOptionsListSelect<>).MakeGenericType(property!.PropertyType));
builder.AddAttribute(1, "Value", Value);
builder.AddAttribute(2, "ValueChanged", changeHandler);
builder.AddAttribute(3, "ValueExpression", lambdaExpression);
builder.AddAttribute(4, "id", FieldId());
builder.AddAttribute(5, "class", "form-control");
builder.CloseComponent();
}
但是如何将此 optionsList 传递给此 InputOptionsListSelect<> ?我不能自己实例化这个组件?据我检查,它需要无参数的构造函数。有什么想法吗?
我试过这样
builder.AddContent(6,ListOptions);
和
private RenderFragment ListOptions => (__builder) =>
{
foreach(var option in this.optionsList!)
{
__builder.OpenElement(7, "option");
__builder.AddAttribute(8, "value", option.Key);
__builder.AddContent(9, option.Value);
__builder.CloseElement();
}
};
但它什么也没做。不知道它放在哪里;P
--------------更新2----------------
是的,我明白了,但这仍然解决了我的问题
我有一个适合你的地方是对的,但他仍然没有解决它。我没有地方放
请检查
和
看看他是怎么用的 InputEnumSelect - 这是我需要做的,但这不是枚举;P
这是我的概念,并在此基础上做了一些修改/添加了一些功能等 所以任何组件中都没有 .razor - 代码中的所有内容
并且初始组件使用是
<DynamicFormComponent
TModel=DataX
Model=d
OnValidSubmitCallback="OnValidSubmit"
ShowValidationSummary=true
ShowValidationUnderField=true>
我发现我可能可以通过 AdditionalAtributes 做到这一点,因为它是 string/object 对 - 不确定它是否是存储列表的最佳位置,但我相信可以通过这种方式完成
感谢和问候!
我不知道您是如何生成表单控件或进行绑定的,但这里介绍了如何执行属性位。我已将其连接成标准形式,以便您可以查看实际代码。
@page "/"
<h3>GenericForm</h3>
<EditForm EditContext=this.editContext>
<InputSelect class="form-select" @bind-Value=modelData.Id>
@this.ListOptions
</InputSelect>
</EditForm>
@code {
public TestModel modelData = new TestModel() { Id = 2 };
private EditContext? editContext;
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(modelData);
return base.OnInitializedAsync();
}
private SortedDictionary<int, string> GetFieldList(string fieldName)
{
var list = new SortedDictionary<int, string>();
var typeInfo = this.modelData.GetType();
var prop = typeInfo.GetProperty(fieldName);
var editorAttr = prop?.GetCustomAttributes(true).ToList().SingleOrDefault(item => item is OptionListAttribute);
if (editorAttr is not null)
{
OptionListAttribute attr = (OptionListAttribute)editorAttr;
var obj = typeInfo.GetProperty(attr.List)?.GetValue(modelData);
if (obj is not null)
list = (SortedDictionary<int, string>)obj;
}
return list;
}
private RenderFragment ListOptions => (__builder) =>
{
@foreach (var option in this.GetFieldList("Id"))
{
<option value="@option.Key">@option.Value</option>
}
};
public class TestModel
{
[OptionList("LookupList")]
public int Id { get; set; }
public SortedDictionary<int, string> LookupList { get; set; } = new SortedDictionary<int, string>()
{
{ 1, "UK" },
{ 2, "France" },
{ 3, "Spain" },
};
}
[AttributeUsage(AttributeTargets.Property)]
public class OptionListAttribute : Attribute
{
public string List { get; set; }
public OptionListAttribute(string list)
{
List = list;
}
}
}
更新
根据更新后的问题,这里有一个自定义组件,显示如何从 ValueExpression
获取模型信息和属性值并构建选项列表。
@using System.Linq.Expressions
@typeparam TValue
<InputSelect @attributes=UserAttributes Value="@this.Value" ValueChanged=this.ValueChanged ValueExpression=this.ValueExpression!>
@this.ListOptions
</InputSelect>
@code {
[Parameter] public TValue? Value { get; set; }
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
private string? fieldName;
private object? model;
private SortedDictionary<TValue, string>? optionList;
protected override void OnInitialized()
{
base.OnInitialized();
if (this.ValueExpression is null)
throw new NullReferenceException("You must set a ValueExpression for the component");
// As we get the ValueExpression we can use it to get the property name and the model
ParseAccessor<TValue>(this.ValueExpression, out model, out fieldName);
// And then get the OptionList
GetOptionList();
}
private void GetOptionList()
{
optionList = new SortedDictionary<TValue, string>();
var typeInfo = this.model?.GetType();
if (typeInfo is not null)
{
var prop = typeInfo.GetProperty(fieldName!);
var editorAttr = prop?.GetCustomAttributes(true).ToList().SingleOrDefault(item => item is OptionListAttribute);
if (editorAttr is not null)
{
OptionListAttribute attr = (OptionListAttribute)editorAttr;
var obj = typeInfo.GetProperty(attr.List)?.GetValue(model);
if (obj is null)
throw new ArgumentException("The provided field must implement the OptionList Attribute.");
optionList = (SortedDictionary<TValue, string>)obj;
}
}
}
// This method takes the Expression provided in ValueExpression and gets the model object and the name of the field referenced
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.");
}
private RenderFragment ListOptions => (__builder) =>
{
@if (this.optionList is not null)
{
@foreach (var option in this.optionList)
{
<option value="@option.Key.ToString()">@option.Value</option>
}
}
};
}
以及修改后的演示页面:
@page "/"
<h3>GenericForm</h3>
<EditForm EditContext=this.editContext>
<div class="p-2">
<MySelect class="form-select" @bind-Value=modelData.Id />
</div>
</EditForm>
@code {
public TestModel modelData = new TestModel() { Id = 2 };
private EditContext? editContext;
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(modelData);
return base.OnInitializedAsync();
}
public class TestModel
{
[OptionList("LookupList")]
public int Id { get; set; }
public SortedDictionary<int, string> LookupList { get; set; } = new SortedDictionary<int, string>()
{
{ 1, "UK" },
{ 2, "France" },
{ 3, "Spain" },
};
}
}
第二次更新
这是在 InputEnumSelect
public sealed class InputListSelect<TValue> : InputBase<TValue>
{
private string? fieldName;
private object? model;
#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
private SortedDictionary<TValue, string>? optionList;
#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
protected override void OnInitialized()
{
base.OnInitialized();
if (this.ValueExpression is null)
throw new NullReferenceException("You must set a ValueExpression for the component");
// As we get the ValueExpression we can use it to get the property name and the model
ParseAccessor<TValue>(this.ValueExpression, out model, out fieldName);
// And then get the OptionList
GetOptionList();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "select");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(4, "onchange", EventCallback.Factory.CreateBinder<string?>(this, value => CurrentValueAsString = value, CurrentValueAsString, culture: null));
if (optionList is not null)
{
foreach (var option in optionList)
{
builder.OpenElement(5, "option");
builder.AddAttribute(6, "value", option.Key?.ToString());
builder.AddContent(7, option.Value);
builder.CloseElement();
}
}
builder.CloseElement(); // close the select element
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
// Let's Blazor convert the value for us
if (BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out TValue? parsedValue))
{
result = parsedValue!;
validationErrorMessage = "";
return true;
}
// Map null/empty value to null if the bound object is nullable
if (string.IsNullOrEmpty(value))
{
var nullableType = Nullable.GetUnderlyingType(typeof(TValue));
if (nullableType != null)
{
result = default!;
validationErrorMessage = "";
return true;
}
}
// The value is invalid => set the error message
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
private void GetOptionList()
{
#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
optionList = new SortedDictionary<TValue, string>();
var typeInfo = this.model?.GetType();
if (typeInfo is not null)
{
var prop = typeInfo.GetProperty(fieldName!);
var editorAttr = prop?.GetCustomAttributes(true).ToList().SingleOrDefault(item => item is OptionListAttribute);
if (editorAttr is not null)
{
OptionListAttribute attr = (OptionListAttribute)editorAttr;
var obj = typeInfo.GetProperty(attr.List)?.GetValue(model);
if (obj is null)
throw new ArgumentException("The provided field must implement the OptionList Attribute.");
optionList = (SortedDictionary<TValue, string>)obj;
}
}
#pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
}
// This method takes the Expression provided in ValueExpression and gets the model object and the name of the field referenced
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.");
}
}