I'm trying to generate at run-time with Blazor a survey based on a definition in a Json file.
I have an interface for all element called IElement
public interface IElement
{
public string? ElementType { get; set; }
public string? Name { get; set; }
}
Then, I have few classes that inherit from it, for example
public class Textbox : IElement
{
[JsonPropertyName("type")]
public virtual string? Type { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("text")]
public string? Text { get; set; }
}
public class Radiobutton : IElement
{
[JsonPropertyName("type")]
public virtual string? Type { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("choises")]
public List<string> Choises = new List<string>();
}
The list of components to display is contained in a list called ElementData
. To show the right HTML component, the interface, I use a switch
like that (it is working):
foreach (var el in ElementData)
{
dynamic newElement = null;
switch (el.ElementType)
{
case ElementType.Checkbox:
Checkbox data = el as Checkbox;
Checkbox test1 = new Checkbox();
test1.CopyTo(data, new[] { "Parent", "Index", "QuestionNumber", "Name" });
newElement = test1;
break;
case ElementType.Textbox:
newElement = el.CreateElement<Textbox>();
Textbox data2 = el as Textbox;
Textbox test2 = new Textbox();
test2.CopyTo(data2, new[] { "Parent", "Index", "QuestionNumber", "Name" });
newElement = test2;
break;
}
}
This code is not very elegant, and for each component I have to change only the type like Checkbox
or Textbox
. I wrote a generic code in T
to replace those lines
public class CreateService<T> where T : class
{
public T CreateElement(IElement el)
{
T data = (T)el;
T test1 = default(T);
CopyTo(test1, data, new[] { "Parent", "Index", "QuestionNumber", "Name" });
return test1;
}
public T CopyTo<T, S>(T target, S source, string[] propertyNames)
{
if (source == null)
return target;
Type sourceType = typeof(S);
Type targetType = typeof(T);
BindingFlags flags = BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance;
PropertyInfo[] properties = sourceType.GetProperties();
foreach (PropertyInfo sPI in properties)
{
if (!propertyNames.Contains(sPI.Name))
{
PropertyInfo tPI = targetType.GetProperty(sPI.Name, flags);
if (tPI != null && tPI.CanWrite && tPI.PropertyType.IsAssignableFrom(sPI.PropertyType))
{
tPI.SetValue(target, sPI.GetValue(source, null), null);
}
}
}
return target;
}
}
but it is not working at all. The error I get is
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Non-static method requires a target.
System.Reflection.TargetException: Non-static method requires a target.
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
at System.Reflection.PropertyInfo.SetValue(Object obj, Object value, Object[] index)
How can I implement correctly the function to create a new element?
I found a solution to my question. I created a function that returns a dynamic
value.
public static dynamic CreateElement(this IElement element) {
dynamic newElement = null;
\\ ...
return newElement;
}
With this function, I define a variable in the newElement
variable and its type could be anything. Then, the function returns this newElement
.