I am building a dynamic form builder using ASP.NET Core MVC and EF Core.
This is just a hobby so I can change it completely without repercussions.
Users are able to enter a form name and description, set the fields, and create the form.
That form can then be accessed via its ID (just an integer for now), filled in, and submitted.
One of its features is that the form's fields can be assigned an element.
This element can be a text box, radio button group, drop-down list. Basically, the element is the type of control that will accept the input.
My models responsible for the form "schema" itself are set up as follows:
I have a class Form which has a list of type Field.
Field has a property of type Element. Several classes, such as TextBox and RadioButtonGroup, inherit from Element.
These derived classes will have different properties, e.g., a textbox will have a placeholder; a radio button group will not.
I have taken this approach because when I create my form fields, I can just use polymorphism to create an Element of type TextBox and have the mapping be done automatically in the database, like so:
Element textBox = new TextBox() { Name = "Default text box", Placeholder = "Enter a value..." };
I also have just one property in Field which caters for everything.
Don't know if there is a better alternative here.
Here are my models:
public class Form
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime? CreatedDate { get; set; }
public List<Field> Fields { get; set; }
}
public class Field
{
public int Id { get; set; }
public string DisplayName { get; set; }
public string? ActualName { get; set; }
public string? DataType { get; set; }
public Element? Element { get; set; }
public bool IsHidden { get; set; } = false;
public Form? Form { get; set; }
}
public class Element
{
public int Id { get; set; }
public string? Name { get; set; }
[ForeignKey("Field")]
public int FieldId { get; set; }
public Field? Field { get; set; }
}
[Table("TextBoxes")]
public class TextBox : Element
{
public string Placeholder { get; set; }
}
Now, this is where I have the problem. Excuse me if my entire design and reasoning is flawed; I am very inexperienced.
When I retrieve a Form to a view (when accessed by ID, for example), EF Core makes it very easy for me to retrieve everything, including the elements belonging to the different fields. The question is: What is the clean way to access the derived class properties?
Currently, in the view, I am using a switch case to check the element type, then downcast the base type to the derived type, and access the properties that way.
I have even managed to do it using reflection before.
Through research, I have found that both of these should be avoided. I have also found something about a visitor pattern; I must admit I have problems understanding how it works and I am not even sure if it applies here.
Currently, this is how I am doing it (ignore the fact that I only have the textbox in the switch in the view):
This is the FormController code for when accessing a created form to submit it.
[HttpGet]
public IActionResult Index(int? id)
{
var submission = new SubmissionViewModel();
submission.Form = _context.Forms.Where(e => e.Id == id).Include(f => f.Fields).ThenInclude(g => g.Element).Include(f => f.EnhFields).FirstOrDefault();
return View(submission);
}
And this is the view code for displaying the form's fields so that you can fill them and eventually submit.
@model WebApplication3.ViewModels.SubmissionViewModel
@using WebApplication3.Models
@{
Layout = "_Layout";
}
<h4>@Model.Form.Name</h4>
<p>@Model.Form.Description</p>
<form asp-controller="Form" asp-action="Submit" class="main-form" method="post">
<input type="hidden" value="@Model.Form.Id" asp-for="@Model.FormId" />
@for (int i = 0; i < Model.Form.Fields.Count; i++)
{
switch (Model.Form.Fields[i].Element.GetType().Name)
{
case "TextBox":
<div>
<label asp-for="@Model.SubmissionFields[Model.Form.Fields[i].ActualName]">@Model.Form.Fields[i].DisplayName</label>
@*<input asp-for="@Model.SubmissionFields[Model.Form.Fields[i].ActualName]" type="text" placeholder="@Model.Form.Fields[i].Element.GetDetails()"/>*@
<input asp-for="@Model.SubmissionFields[Model.Form.Fields[i].ActualName]" type="text" placeholder="@(((TextBox)Model.Form.Fields[i].Element).Placeholder)"/>
</div>
break;
}
}
<button type="submit">Submit</button>
</form>
So, down-casting/reflection make my life easier, but are not good practice.
What is the proper (if any) alternative way to do this?
Sorry if the question does not make sense or is not easily understandable.
I don't use Stack Overflow much but I will try to make it better if necessary.