I am using Blazor to create a dynamic form from the properties of my model.
I am using a for each loop to loop through the properties of the model.
public class SensorType
{
public int Id { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Sensor Type Name")]
public string Name { get; set; }
[Required]
[MaxLength(500)]
public string Description { get; set; }
[Required]
[MaxLength(2048)]
[Display(Name = "Datasheet URL")]
public string DatasheetUrl { get; set; }
}
I implemented this razor view, where I try to bind to public SensorType sensortype { get; set; }. But I need to bind to sensortype.property where property is whatever property the model has that is in the for each loop. But I cannot simply just call say #bind-Value="sensortype.property". Any ideas on how to do this? I don't want to have to manually type every field. Thanks!
<EditForm Model="#sensortype" OnValidSubmit="#SaveSensorType">
#foreach(var property in typeof(SensorType).GetProperties())
{
if(property.Name == "Id")
{
continue;
}
<div class="form-group row">
<label class="col-sm-2 col-form-label">#(GetAttribute(property, false)) </label> //This function get the name of the property in a human-readable way.
<div class="col-sm-10">//I would like to bind here to sensortype's property in the for each loop but sensortype.property gives me an error.
<InputTextArea class="form-control" #bind-Value="sensortype.property" value="sensortype.property.Name" placeholder="Description of the Type of Sensor" />
</div>
</div>
}
I am playing this days with blazor and I made a kind of DynamicForm, is not perfect but is working. I just want to show you my dynamic form as proof of concept, not at something which I wold use in production.
Basicly we want to write something like this:
<DynamicForm #bind-Model="MySensorType" />
//this will generate a form with fields for all properties of the model
So, on Index view let's create a property for MySensorType and some markup to see if the model is changing when the form fields are edited.
#page "/"
<div style="display:flex">
<div>
<DynamicForm #bind-Model="MySensorType" />
</div>
<div style="background:yellow;flex:1;margin:20px;">
<p>Id: #MySensorType.Id</p>
<p>Name: #MySensorType.Name</p>
<p>Description: #MySensorType.Description</p>
<p>Url: #MySensorType.DatasheetUrl</p>
</div>
</div>
#code {
public SensorType MySensorType { get; set; } = new SensorType();
public class SensorType
{
public int Id { get; set; } = 1;
public string Name { get; set; } = "Some Name";
public string Description { get; set; } = "Some Description";
public string DatasheetUrl { get; set; } = "This is a URL";
}
}
In order to generate automatically the fields, we need some kind of DynamicField.
Below component is for named "DynamicField"
<div>
<label>#Caption</label>
#if (Value is String sValue)
{
<input type="text" value="#sValue" #onchange="OnChange"/>
}
#if (Value is int iValue)
{
<input type="number" value="#iValue" #onchange="OnChange" />
}
</div>
#code {
[Parameter] public string Caption { get; set; }
[Parameter] public object Value { get; set; }
[Parameter] public EventCallback<object> ValueChanged { get; set; }
async void OnChange(ChangeEventArgs e)
{
await ValueChanged.InvokeAsync(e.Value);
}
}
Now, we can create the wrapper so called DynamicForm:
#typeparam T
#foreach (var p in Properties)
{
<DynamicField Value="#p.Value" Caption="#p.Key" ValueChanged="#((e)=>OnValueChanged(e,p.Key))"/>
}
#code{
[Parameter] public T Model { get; set; }
[Parameter] public EventCallback<T> ModelChanged { get; set; }
public Dictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();
protected override void OnInitialized()
{
var props = Model.GetType().GetProperties();
foreach (var p in props)
{
Properties.Add(p.Name, p.GetValue(Model));
}
}
void OnValueChanged(object e, string prop)
{
var p = Model.GetType().GetProperty(prop);
if (p.PropertyType == typeof(int))
{
var intValue = Convert.ToInt32(e);
p.SetValue(Model, intValue);
}
if (p.PropertyType == typeof(string))
{
p.SetValue(Model, e.ToString());
}
ModelChanged.InvokeAsync(Model);
}
}
What actually is happening here, we are using Reflection to get all properties of the model, send them to DynamicFields, and when those values are changed we set the new value to the model and call ModelChanged to send new values.
On my computer this works, and every time when I change a value, MySensorType is showing the new value on Index component.
You can see that I created dynamic fields only for Number and String, if you have DateTime or Select, you need to extend this DynamicField, for select will be more difficult.
By the way, on Index view you can put a button and call SaveChanges with your logic and use MySensorType.
Related
I'm attempting to loop through a model and create a form component for each property in the model. I am continuing to encounter various errors and need some help. Here is the relevent code:
forms.razor
<div class="form-section">
#foreach (var property in DataModel.GetType().GetProperties())
{
var propertyString = $"DataModel.{property.Name.ToString()}";
#if (property.PropertyType == typeof(DateTime))
{
<InputDate id=#"property.Name" #bind-Value="#propertyString">
}
else if (property.PropertyType == typeof(int))
{
<InputNumber id=#"property.Name" #bind-Value="#propertyString">
}
else
{
<InputText id=#"property.Name" #bind-Value="#propertyString">
}
}
</div>
DataModel.cs
public class DataModel
{
public DateTime SelectedData { get; set; }
public int SelectedNumber { get; set; }
public decimal SelectDecimal { get; set; }
}
Obviously, I've simplified the code greatly. The div in forms.razor shown above is nested in an EditForm element. This is looping through the properties of the class DataModel, and if the type is a DateTime, it should render an InputDate form, or an InputNumber form, etc. If there were only 3 properties I would just hard-code them, but in the actual application I have over 100 properties of either DateTime, int, or decimal types, so I'm looking for a more programmatic solution. My goal is to get this to check the type of each property in the class, then correctly render the appropriate form associate with that data type, bound to the correct property. Presently, I can only get the InputDate form to render, but when I enter a date in the form I get the following exception:
Error: System.InvalidOperationException: The type 'System.String' is not a supported date type
I'm guessing this is due to the fact that I'm passing in a string of the property name in the #bind-Value parameter of the InputDate component. I cannot figure out to pass the actual reference to the property though. Can anyone help me to hone in on how to resolve this issue?
I finally figured this one out. I ended up using a callback in the onchange attribute that set the value of the current property in the loop.
<div class="form-section">
#foreach (var property in DataModel.GetType().GetProperties())
{
var propertyString = $"DataModel.{property.Name.ToString()}";
#if (property.PropertyType == typeof(DateTime))
{
<input type="date" id="#property.Name" #onchange="#(e => property.SetValue(dataModel, DateTime.Parse(e.Value.ToString())))" />
}
else if (property.PropertyType == typeof(int))
{
<input type="number" id="#property.Name" #onchange="#(e => property.SetValue(dataModel, Int32.Parse(e.Value.ToString())))" />
}
else
{
<input type="number" id="#property.Name" #onchange="#(e => property.SetValue(dataModel, Decimal.Parse(e.Value.ToString())))" />
}
}
</div>
I think what #nocturns2 said is correct, you could try this code:
#if (property.PropertyType == typeof(DateTime))
{
DateTime.TryParse(YourPropertyString, out var parsedValue);
var resutl= (YourType)(object)parsedValue
<InputDate id=#"property.Name" #bind-Value="#resutl">
}
Here's a componentized version with an example component for DateOnlyFields
#using System.Reflection
#using System.Globalization
<input type="date" value="#GetValue()" #onchange=this.OnChanged #attributes=UserAttributes />
#code {
[Parameter] [EditorRequired] public object? Model { get; set; }
[Parameter] [EditorRequired] public PropertyInfo? Property { get; set; }
[Parameter] public EventCallback ValueChanged { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
private string GetValue()
{
var value = this.Property?.GetValue(Model);
return value is null
? DateTime.Now.ToString("yyyy-MM-dd")
: ((DateOnly)value).ToString("yyyy-MM-dd");
}
private void OnChanged(ChangeEventArgs e)
{
if (BindConverter.TryConvertToDateOnly(e.Value, CultureInfo.InvariantCulture, out DateOnly value))
this.Property?.SetValue(this.Model, value);
else
this.Property?.SetValue(this.Model, DateOnly.MinValue);
if (this.ValueChanged.HasDelegate)
this.ValueChanged.InvokeAsync();
}
}
And using it:
#foreach (var property in dataModel.GetType().GetProperties())
{
#if (property.PropertyType == typeof(DateOnly))
{
<div class="form-text">
<DateOnlyEditor class="form-control" Model=this.dataModel Property=property ValueChanged=this.DataChanged />
</div>
}
}
<div class="p-3">
Date: #dataModel.SelectedDate
</div>
#code {
private DataModel dataModel = new DataModel();
private EditContext? editContext;
public class DataModel
{
public DateOnly SelectedDate { get; set; } = new DateOnly(2020, 12, 25);
public int SelectedNumber { get; set; }
public decimal SelectDecimal { get; set; }
}
protected override void OnInitialized()
{
this.editContext = new EditContext(dataModel);
base.OnInitialized();
}
void DataChanged()
{
StateHasChanged();
}
}
I am trying to have this input radio button checked base on the model but I am getting string error. Is this the correct way to set the checked enable?
Error
'string' does not contain a definition for 'Regimens'
debugger
View
<label for="RegimenReferencesC_#Model">
<input type="radio" id="RegimenReferencesC_#Model" checked="#(Model.Regimens == (int)RegimenReferences.D ? "true" : "false")" name="RegimenReferences" value="#((int)RegimenReferences.C)" class="regimen-reference">
(c) #RegimenReferences.C.ToDescriptionString()
</label>
Tried it this way too
<label for="RegimenReferencesD_#Model">
<input type="radio" id="RegimenReferencesD_#Model" name="RegimenReferences" checked="#(Model.Regimens == (int)RegimenReferences.D ? true : false)" value="#((int)RegimenReferences.D)" class="regimen-reference">
(d) #RegimenReferences.D.ToDescriptionString()
</label>
Model
public class ReferencesModel
{
public long id { get; set; }
public string Link { get; set; }
public string Text { get; set; }
public string Type { get; set; }
public int Regimens { get; set; }
public Guid? GuidelineId { get; set; }
public int SortOrder { get; set; }
}
It seems clear from <label for="RegimenReferencesC_#Model"> that #Model in the Razor view is a string, or that line would not work.
Therefore, when you try #(Model.Regimens ..., you get the error, since a string does not have a property or method named Regimens.
In other words, check your Model in the view. It is probably a string and not the object you are looking for.
You are missing some relevant code in your post so I am going to fill in the blanks with assumptions:
Given the following Class and Enum:
public class ReferencesModel
{
public long id { get; set; }
public string Link { get; set; }
public string Text { get; set; }
public string Type { get; set; }
public int Regimens { get; set; }
public Guid? GuidelineId { get; set; }
public int SortOrder { get; set; }
}
public enum RegimenReferences
{
[Description("This is A")]
A = 0,
[Description("This is B")]
B = 1,
[Description("This is C")]
C = 2,
[Description("This is D")]
D = 3
}
and the following Helper method for displaying enum annotation text:
public static string ToDescriptionString<T>(this T e) where T : IConvertible
{
if (e is Enum)
{
Type type = e.GetType();
Array values = System.Enum.GetValues(type);
foreach (int val in values)
{
if (val == e.ToInt32(CultureInfo.InvariantCulture))
{
var memInfo = type.GetMember(type.GetEnumName(val));
var descriptionAttribute = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute;
if (descriptionAttribute != null)
{
return descriptionAttribute.Description;
}
}
}
}
return null;
}
Your Razor code would be as follows to use a radio button and base checked on the value:
<label for="RegimenReferencesD_#Model">
<input type="radio" id="RegimenReferencesC_#Model" #if (Model.Regimens == (int)RegimenReferences.D) { Html.Raw("checked"); } name="RegimenReferences" value="#((int)RegimenReferences.D)" class="regimen-reference">
(D) #RegimenReferences.D.ToDescriptionString()
</label>
I am trying to bind a model field to a control, I have it working to the form but I'm trying to refactor the components so that I can reused the control and it's attributes throughout the program.
Here are some snippets to try to portray my situation:
Sample Model
public class MyModel
{
public DateTime DateOpened { get; set; }
}
Used in MyModelFormBase.cs
public class MyModelFormBase : ComponentBase
{
protected MyModal data = new MyModel();
}
Used in MyModelForm.razor
<MyForm Model="#data" AddFunction="#InsertMyModel" DataDismiss="MyModelForm">
<MyDateInput InputId="dateOpened" InputName="Date Opened" #bind-InputValue="#data.DateOpened" />
</MyForm>
MyForm.razor, uses blazorstrap
<BSForm Model="#Model" OnValidSubmit="#AddFunction"
#ChildContent
<BSButton Color="Color.Primary"
ButtonType="ButtonType.Submit"
data-dismiss="#DataDismiss">
Submit</BSButton>
</BSForm>
#code {
[Parameter]
public Object Model { get; set;}
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback AddFunction { get; set; }
[Parameter]
public string DataDismiss { get; set; }
}
MyDateInput.razor, uses blazorstrap
<BSFormGroup>
<BSLabel For="#InputId">#InputName</BSLabel>
<BSInput InputType="InputType.Date" Id="#InputId"
#bind-Value="#InputValue"
ValidateOnChange="true"
#onchange="UpdateData"
/>
<BSFormFeedback For="#(() => InputValue)"/>
</BSFormGroup>
#code {
[Parameter]
public DateTime InputValue { get; set; }
[Parameter]
public EventCallback<DateTime> InputValueChanged { get; set; }
[Parameter]
public string InputId { get; set; }
[Parameter]
public string InputName { get; set; }
private async Task UpdateData()
{
await InputValueChanged.InvokeAsync(InputValue);
}
}
The default data provided by my service is correctly displayed in the control, so it's properly bound downwards, but doesn't propagate any changes back up to the model. :(
The goal is to be able to keep a MyDateInput Component that is repeated throughout the application that is bound 2 ways with the model regardless of layers of components it's passed through.
Any ideas?
It's really quite daunting to answer your question with so much code missing or unknown. However, I'll try to provide some assistance with the hope it can help you solve your ailments.
This code:
await InputValueChanged.InvokeAsync(InputValue);
I guess is a callback to update the value of the bound property MyModel.DateOpened , right ? If not it should be doing that. In any case, this property should be annotated with the PropertyAttribute attribute...
This may be the reason why two-way data binding is not working.
Note that your MyDateInput componet may also need to receive a ValueExpression value. I can't say that for sure, because not all your code is available, so take as a word of warning:
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
I'll put at the end of my answer a small sample to make this clear.
Are you sure this is working? The compiler should be barking:
#bind-Value="#InputValue" ValidateOnChange="true" #onchange="UpdateData"
telling you that you are using two onchange events. This should be, ordinarily
value="#InputValue" ValidateOnChange="true" #onchange="UpdateData", but I cannot be certain about it as I don't see how BSInput is implemented...
Note: BSForm is missing a closing tag before #ChildContent
Sample code:
RazorInputTextTest.razo
-----------------------
#page "/RazorInputTextTest"
<span>Name of the category: #category.Name</span>
<EditForm Model="#category">
<RazorInputText Value="#category.Name" ValueChanged="#OnValueChanged" ValueExpression="#(() => category.Name)" />
</EditForm>
#code{
private Category category { get; set; } = new Category { ID = "1", Name = "Beverages" };
private void OnValueChanged(string name)
{
category.Name = name;
}
}
RazorInputText.razor
--------------------
<div class="form-group">
<InputText class="form-control" #bind-Value="#Value"></InputText>
</div>
#code{
private string _value { get; set; }
[Parameter]
public string Value
{
get { return _value; }
set
{
if (_value != value) {
_value = value;
if (ValueChanged.HasDelegate)
{
ValueChanged.InvokeAsync(value);
}
}
}
}
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
}
Hope this helps...
I am new to MVC (coming from WebForm background). I have a main view which contains 2 tabs that need the same model for table view. However, I created a partial view containing table structure to display data records in the 2 tabs asynchronously. One tab gets its data from CSV while the other gets from SQL Server based on user's selection but having the same model.
I have the screenshot of the concept below: The box in red depicts the Partial View, it has 2 buttons, one to commit the table data into the database while the other is to add more records if needed;
Partial view in Parent View screenshot
My challenge is: I have a class method that needs to get a CSV file from FileUpload value and binds data to the model on the partial view.
See my model structure below:
[Table("atm")]
public class ATM
{
public ATM()
{
this._EJTransactions = new HashSet<EJTransaction>();
this._CassetteSettings = new HashSet<CassetteSetting>();
this._EJFileDownloads = new HashSet<EJFileDownload>();
this._CamFileDownloads = new HashSet<ImageFileDownload>();
this._TransImages = new HashSet<TransImage>();
this._UserAtms = new HashSet<UserAtms>();
}
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int atmId { get; set; }
[ForeignKey("ATMVendor")]
public int vendorId { get; set; }
[ForeignKey("BankBranch")]
public int branchId { get; set; }
[MaxLength(50)]
[Index("ix_uniq_terminal", 1, IsUnique = true)]
[DisplayName("Terminal ID")]
public string terminalId { get; set; }
[MaxLength(30)]
[Index("ix_uniq_ip", 1, IsUnique = true)]
[DisplayName("IP")]
public string ip { get; set; }
[MaxLength(100)]
[Index("ix_uniq_title", 1, IsUnique = true)]
[DisplayName("Title")]
public string title { get; set; }
[DisplayName("EJ Enabled")]
public bool ejEnabled { get; set; }
[DisplayName("Image Enabled")]
public bool camEnabled { get; set; }
[DisplayName("IsActive")]
public bool isActive { get; set; }
public virtual ATMVendor ATMVendor { get; set; }
public virtual BankBranch BankBranch { get; set; }
public virtual ICollection<EJTransaction> _EJTransactions { get; set; }
public virtual ICollection<CassetteSetting> _CassetteSettings { get; set; }
public virtual ICollection<EJFileDownload> _EJFileDownloads { get; set; }
public virtual ICollection<ImageFileDownload> _CamFileDownloads { get; set; }
public virtual ICollection<TransImage> _TransImages { get; set; }
public virtual ICollection<UserAtms> _UserAtms { get; set; }
}
The partial view binds to this model.
public class CSVAtmLoader
{
public IEnumerable<ATM> Read(string csvfile)
{
List<ATM> atmlist = new List<ATM>();
TextReader csvReader = new StreamReader(csvfile);
var csv = new CsvReader(csvReader, false);
csv.Configuration.DetectColumnCountChanges = true;
csv.Configuration.RegisterClassMap<AtmMap>();
//csv.Configuration.InjectionCharacters = new[] { '=', '#', '+', '-' };
//csv.Configuration.SanitizeForInjection = false;
//csv.Configuration.InjectionEscapeCharacter = '\t';
var atms = csv.GetRecords<ATM>();
foreach (var atm in atms)
{
atm.branchId = GetBranchId(atm.BankBranch.branch);
atm.vendorId = GetVendorId(atm.ATMVendor.vendor);
atmlist.Add(atm);
}
return atmlist;
}
private int GetBranchId(string branch)
{
BankBranch br = new BankBranch { branch = branch }.SelectBranch();
return 0;
}
private int GetVendorId(string vendor)
{
return 0;
}
}
In the parent view, I have a CSVAtm tab that hosts the partial view, I am headway blocked to get this done with dynamism of the layout. See my parent view that render the partial view with csvfile chosen from FileUpload control:
<div class="tab-pane fade" id="csvAtm">
<p>
<div class="form-inline">
<table>
<tr>
<td><span> Choose a File:</span> </td>
<td><input id="csvfile" type="file" name="file" class="btn btn-default" onchange="clear();"/></td>
<td> </td>
<td>
<input class="btn btn-default col-mid-2" type="button" id="upload" value="Upload" onclick="uploadfile();" />
</td>
</tr>
</table>
</div>
<div id="error"></div>
<br />
<div class="form-group">
#{
if (string.IsNullOrEmpty(filename))
{
//this is where my challenge is, how to get the filename from fileupload
//I know this is executed at the server side and hence, the client is not aware of server side scripting
Html.RenderPartial("LoadedAtms", new CSVAtmLoader().Read(filename));
}
}
</div>
I need a better way to achieve this with fileupload security in mind, my approach might be wrong. If I can achieve this successfully in the csv tab, it will be easier to replicate same for connecting to Sql server.
Thanks so much..
I have a view model like so:
public class ListingPlanEditorViewModel
{
public ListingPlan Plan { get; set; }
public IEnumerable<Directory> SiteDirectories { get; set; }
}
One property is an object of type ListingPlan here:
public class ListingPlan
{
public int? ListingPlanID { get; set; }
public int DescriptionLinesCount { get; set; }
public List<Directory> Directories { get; set; }
}
The object Directory looks like this:
public class Directory
{
public int DirectoryID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
I have a controller that returns a ListingPlanEditorViewModel to the view:
public ActionResult ConfigurePlan(int? listingIdentifier)
{
ListingPlan plan = new ListingPlan()
{
DescriptionLinesCount = 10,
Directories = new List<Directory>()
{
new Directory()
{
DirectoryID = 3
},
new Directory()
{
DirectoryID = 4
}
}
};
ListingPlanEditorViewModel model = new ListingPlanEditorViewModel()
{
Plan = plan,//_listingRepository.GetListingPlan(listingIdentifier, null),
SiteDirectories = _database.GetDirectories()
};
return View(model);
}
I would like to create a multiselect box that will bind the selected values back to the Plan property in the ListingPlanEditorViewModel, setting the DirectoryID property for each selection. So after binding I should have a List of Directory objects. All with their DirectoryID's set.
I'm having some trouble doing this. I can create the multiselectbox with the correct select options in it, but I am unable to retrieve them in my post action which looks like this:
#using (Html.BeginForm("ConfigurePlan", "ListingPlan"))
{
<div class="form-body">
#Html.ListBoxFor(model => model.Plan.Directories, new MultiSelectList(Model.SiteDirectories, "DirectoryID", "Name"))
</div>
<button type="submit">submit</button>
}
You have to create an [] or List of IDs in the ViewModel that will store selected values.
public class ListingPlanEditorViewModel
{
public ListingPlan Plan { get; set; }
public IEnumerable<Directory> SiteDirectories { get; set; }
public int[] DirectoryIDs {get;set;}
}
The View will change according. The Directories selected will be stored in DirectoryIDs.
#using (Html.BeginForm("ConfigurePlan", "ListingPlan"))
{
<div class="form-body">
#Html.ListBoxFor(model => model.DirectoryIDs, new MultiSelectList(Model.SiteDirectories, "DirectoryID", "Name"))
</div>
<button type="submit">submit</button>
}
Now on POST Action you can query the database and get the Directories that was selected by user.
Note: You can't just get the full objects because the ListBoxFor will generate a <select multiple ... > ... </select> tag won't know how to bind to your object.