EDIT: So it turns out that my ModelBinder
isn't actually the problem. The issue is with my ValueConverter
. Leaving this here, as I think it illustrates how to do a custom model binder though.
There is a lot of code in this question. I'm trying to be as thorough and precise as possible.
I have a Measurement
class. Many of my models have one or more properties of type Measurement
. This class maps to the database as a string using a ValueConverter
. What I would like to do is convert form data, namely strings, to this type upon form submission. So, I created a custom ModelBinder
.
public class MeasurementModelBinder : IModelBinder
{
public MeasurementModelBinder() { }
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if(bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); }
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if(valueProviderResult == ValueProviderResult.None) { throw new Exception(); }
string valueString = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(valueString)) { throw new Exception(); }
Measurement result = new Measurement(valueString);
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
I would like this MeasurementModelBinder
to apply to all properties of this type in every model. So I'm trying to do that. Here's an example of a model that has Measurement
type properties.
public class Material
{
[Key]
public int Key { get; set; }
public string Name { get; set; }
public Measurement Density { get; set; }
public Measurement Conductivity { get; set; }
[Display(Name="Specific Heat")]
public Measurement SpecificHeat { get; set; }
public string Description { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
public Material() { }
public Material(string name, Measurement density, Measurement conductivity, Measurement specificHeat, string description = "")
{
Name = name;
Density = density;
Description = description;
Conductivity = conductivity;
SpecificHeat = specificHeat;
}
public override string ToString() { return Name; }
}
As for the controller, I'm just using a test controller right now, a modified version of one of Visual Studio's auto-generated MVC Entity Framework controller with views. Here's one of the actions in the controller.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Material material)
{
material.Created = DateTime.Now;
material.Updated = DateTime.Now;
if (ModelState.IsValid)
{
_context.Add(material);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(material);
}
The Created
and Updated
fields are not provided by the form, so I fill them in myself. However, everything else is filled in by the form, as strings. You can see how a Measurement
object is constructed in the custom model binder class up above.
I have created a ModelBinderProvider
class, seen below.
public class MeasurementModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if(context == null) { throw new ArgumentException(nameof(context)); }
if(!context.Metadata.IsComplexType && context.Metadata.ModelType == typeof(Measurement))
{
return new MeasurementModelBinder();
}
return null;
}
}
And I have registered the provider in the Startup.ConfigureServices()
method, like this.
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new MeasurementModelBinderProvider());
});
This is the form that is being submitted.
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<input asp-for="Description" class="form-control" />
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Density" class="control-label"></label>
<input asp-for="Density" class="form-control" />
<span asp-validation-for="Density" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Conductivity" class="control-label"></label>
<input asp-for="Conductivity" class="form-control" />
<span asp-validation-for="Conductivity" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SpecificHeat" class="control-label"></label>
<input asp-for="SpecificHeat" class="form-control" />
<span asp-validation-for="SpecificHeat" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
I would like the submission of this form to add a new record to the material table with all fields filled in. It doesn't do that. Instead, it adds a new record where all the Measurement
properties are null.
I have tried adding the [ModelBinder(BinderType = typeof(MeasurementModelBinder))]
attribute to the Measurement
class and that did not help. In fact, it caused an "Unable to resolve service" error, and when I got rid of that error by adding services.AddScoped<IModelBinder, MeasurementModelBinder>();
to ConfigureServices()
, I still got the same result from my action.
I have also tried adding the [BindProperty(BinderType typeof(MeasurementModelBinder))]
attribute to the properties in the model themselves, and it doesn't help. Furthermore, that is something I would rather not do. Is there any way to just make it work on all Measurement
type properties? If not, is there any way to just make it work at all?
Thanks.
I tried your code and when I add the [ModelBinder(BinderType = typeof(MeasurementModelBinder))]
attribute to the Measurable
class, it worked for me. I think you should add the [ModelBinder(BinderType = typeof(MeasurementModelBinder))]
attribute to the Measurement
class.