Using HtmlString throws an InvalidOperationException when updating the model - asp.net-mvc

I'm using an HtmlString property on my model like this
public HtmlString Html { get; set; }
then I have an EditorTemplate that renders and html editor but when I use TryUpdateModel() I get an InvalidOperationException because no type converter can convert between these types String and HtmlString.
Do I need to create a custom model binder or is there another way?
UPDATE:
I'm trying to use HtmlString on my model, mostly for making it obvious that it contains HTML.
So this is what my complete model looks like:
public class Model {
public HtmlString MainBody { get; set; }
}
and this is how I render the form:
#using (Html.BeginForm("save","home")){
#Html.EditorForModel()
<input type="submit" name="submit" />
}
I have created my own editor template called Object.cshtml so that the field MainBody can be rendered as a textarea.
My controller has a Save method that looks like this:
public void Save([ModelBinder(typeof(FooModelBinder))]Model foo) {
var postedValue = foo.MainBody;
}
As you can see I have been playing around with a custom model binder that looks like this:
public class FooModelBinder : DefaultModelBinder {
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) {
if (propertyDescriptor.PropertyType == typeof(HtmlString)) {
return new HtmlString(controllerContext.HttpContext.Request.Form["MainBody.MainBody"]);
}
return null;
}
}
this works as expected but I don't know how to get the complete ModelName from the bindingContext because bindingContext.ModelName only contains the MainBody and not MainBody.MainBody?
I'm also interested in other solutions regarding this or maybe if someone thinks it's a really bad idea.

Do I need to create a custom model binder
Yes, if you want to use an HtmlString property on your view model because this class has no parameterless constructor and the default model binder has no clue how to instantiate it.
or is there another way?
Yes, don't use HtmlString property on the view model. There might also be other ways. Unfortunately since you have provided strictly 0 information about your context and what precisely you are trying to achieve that's all we could help you with so far.
UPDATE:
Now that you have shown a wee-bit of your code here's a sample.
Model:
public class Model
{
public HtmlString MainBody { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new Model());
}
[HttpPost]
public ActionResult Index([ModelBinder(typeof(FooModelBinder))]Model foo)
{
var postedValue = foo.MainBody;
return Content(postedValue.ToHtmlString(), "text/plain");
}
}
Model binder:
public class FooModelBinder : DefaultModelBinder
{
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
if (propertyDescriptor.PropertyType == typeof(HtmlString))
{
return new HtmlString(bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue);
}
return null;
}
}
View (~/Views/Home/Index.cshtml):
#using (Html.BeginForm())
{
#Html.EditorForModel()
<input type="submit" name="submit" />
}
Custom object editor template in order to do a deep dive (~/Views/Shared/EditorTemplates/Object.cshtml):
#foreach (var property in ViewData.ModelMetadata.Properties.Where(x => x.ShowForEdit))
{
if (!string.IsNullOrEmpty(property.TemplateHint))
{
#Html.Editor(property.PropertyName, property.TemplateHint)
}
else
{
#Html.Editor(property.PropertyName)
}
}
Custom editor template for the HtmlString type to be rendered as a textarea (~/Views/Shared/EditorTemplates/HtmlString.cshtml):
#Html.TextArea("")
By the way I still don't understand why you would want to use HtmlString as a property instead of a simple string but anyway.

Related

MVC Model Binding with Checkbox list....possibly haunted [duplicate]

I have cut the model back to one field:
//Model
public class LetterViewModel
{
public string LetterText;
}
//Controller
public ActionResult Index()
{
var model = new LetterViewModel();
model.LetterText = "Anything";
return View(model);
}
[HttpPost]
public ActionResult Index(LetterViewModel model)
{
//model.LetterText == null
return View(model);
}
//view
#model Test.Models.LetterViewModel
#{
Layout = "~/Views/Shared/_Layout.cshtml";
ViewBag.Title = "Create a Letter";
}
#using (Html.BeginForm())
{
<div id="Bottom">
#Html.TextAreaFor(m => m.LetterText)
<input type="submit" value="Ok" class="btn btn-default" />
</div>
}
When I check the Network tab in dev tools it is showing the value entered is being included in the request. However, when the HttpPost controller is fired the field is empty.
The DefaultModelBinder does not set the value of fields, only properties. You need to change you model to include properties
public class LetterViewModel
{
public string LetterText { get; set; } // add getter/setter
}
You can also use a custom binder instead of the default binder in cases where you don't have the option to convert fields to properties.
Loop through the form inputs and set them using reflection. MemberInformation is my class but you can just use FieldInfo.
This doesn't do an object graph but if I need that ability I'll enhance my answer. The tuple in the foreach uses c# 7.0. It also assumes that you saved your object from the previous GET before this POST.
using CommonBusinessModel.Metadata;
using GHCOMvc.Controllers;
using System;
using System.Linq;
using System.Web.Mvc;
namespace AtlasMvcWebsite.Binders
{
public class FieldModelBinder : DefaultModelBinder
{
// this runs before any filters (except auth filters)
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var form = controllerContext.HttpContext.Request.Form;
Type type = typeof(GHCOBusinessModel.GHCOPPAType);
AtlasKernelBusinessModel.VersionedObject instance = PolicyController.Policy;
foreach ((var value, var member) in (from string input in form
let fi = type.GetField(input)
where fi != null
let mi = new MemberInformation(fi, instance)
where !mi.ReadOnly
select (form[input], mi)))
member.SetValue(value);
return instance;
}
}
}
You need to add [FromBody] before parameter to Action
[HttpPost]
public ActionResult Index([FromBody]LetterViewModel model)
{
//model.LetterText == null
return View(model);
}

Why isn't this simple MVC form posting the model back? [duplicate]

I have cut the model back to one field:
//Model
public class LetterViewModel
{
public string LetterText;
}
//Controller
public ActionResult Index()
{
var model = new LetterViewModel();
model.LetterText = "Anything";
return View(model);
}
[HttpPost]
public ActionResult Index(LetterViewModel model)
{
//model.LetterText == null
return View(model);
}
//view
#model Test.Models.LetterViewModel
#{
Layout = "~/Views/Shared/_Layout.cshtml";
ViewBag.Title = "Create a Letter";
}
#using (Html.BeginForm())
{
<div id="Bottom">
#Html.TextAreaFor(m => m.LetterText)
<input type="submit" value="Ok" class="btn btn-default" />
</div>
}
When I check the Network tab in dev tools it is showing the value entered is being included in the request. However, when the HttpPost controller is fired the field is empty.
The DefaultModelBinder does not set the value of fields, only properties. You need to change you model to include properties
public class LetterViewModel
{
public string LetterText { get; set; } // add getter/setter
}
You can also use a custom binder instead of the default binder in cases where you don't have the option to convert fields to properties.
Loop through the form inputs and set them using reflection. MemberInformation is my class but you can just use FieldInfo.
This doesn't do an object graph but if I need that ability I'll enhance my answer. The tuple in the foreach uses c# 7.0. It also assumes that you saved your object from the previous GET before this POST.
using CommonBusinessModel.Metadata;
using GHCOMvc.Controllers;
using System;
using System.Linq;
using System.Web.Mvc;
namespace AtlasMvcWebsite.Binders
{
public class FieldModelBinder : DefaultModelBinder
{
// this runs before any filters (except auth filters)
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var form = controllerContext.HttpContext.Request.Form;
Type type = typeof(GHCOBusinessModel.GHCOPPAType);
AtlasKernelBusinessModel.VersionedObject instance = PolicyController.Policy;
foreach ((var value, var member) in (from string input in form
let fi = type.GetField(input)
where fi != null
let mi = new MemberInformation(fi, instance)
where !mi.ReadOnly
select (form[input], mi)))
member.SetValue(value);
return instance;
}
}
}
You need to add [FromBody] before parameter to Action
[HttpPost]
public ActionResult Index([FromBody]LetterViewModel model)
{
//model.LetterText == null
return View(model);
}

Custom Model Binder Not Validating Model

I started to play around with knockout.js and in doing so I used the FromJsonAttribute (created by Steve Sanderson). I ran into an issue with the custom attribute not performing model validation. I put together a simple example-- I know it looks like a lot of code-- but the basic issue is how to force the validation of the model within a custom model binder.
using System.ComponentModel.DataAnnotations;
namespace BindingExamples.Models
{
public class Widget
{
[Required]
public string Name { get; set; }
}
}
and here is my controller:
using System;
using System.Web.Mvc;
using BindingExamples.Models;
namespace BindingExamples.Controllers
{
public class WidgetController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(Widget w)
{
if(this.ModelState.IsValid)
{
TempData["message"] = String.Format("Thanks for inserting {0}", w.Name);
return RedirectToAction("Confirmation");
}
return View(w);
}
[HttpPost]
public ActionResult PostJson([koListEditor.FromJson] Widget w)
{
//the ModelState.IsValid even though the widget has an empty Name
if (this.ModelState.IsValid)
{
TempData["message"] = String.Format("Thanks for inserting {0}", w.Name);
return RedirectToAction("Confirmation");
}
return View(w);
}
public ActionResult Confirmation()
{
return View();
}
}
}
My issue is that the model is always valid in my PostJson method. For completeness here is the Sanderson code for the FromJson attribute:
using System.Web.Mvc;
using System.Web.Script.Serialization;
namespace koListEditor
{
public class FromJsonAttribute : CustomModelBinderAttribute
{
private readonly static JavaScriptSerializer serializer = new JavaScriptSerializer();
public override IModelBinder GetBinder()
{
return new JsonModelBinder();
}
private class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
if (string.IsNullOrEmpty(stringified))
return null;
var model = serializer.Deserialize(stringified, bindingContext.ModelType);
return model;
}
}
}
}
Description
The FromJsonAttribute only binds to the model and does, like you said, no validation.
You can add validation to the FromJsonAttribute in order to validate the model's against his DataAnnotations attributes.
This can be done using the TypeDescriptor class.
TypeDescriptor Provides information about the characteristics for a component, such as its attributes, properties, and events.
Check out my solution. I have tested it.
Solution
private class JsonModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var stringified = controllerContext.HttpContext.Request[bindingContext.ModelName];
if (string.IsNullOrEmpty(stringified))
return null;
var model = serializer.Deserialize(stringified, bindingContext.ModelType);
// DataAnnotation Validation
var validationResult = from prop in TypeDescriptor.GetProperties(model).Cast<PropertyDescriptor>()
from attribute in prop.Attributes.OfType<ValidationAttribute>()
where !attribute.IsValid(prop.GetValue(model))
select new { Propertie = prop.Name, ErrorMessage = attribute.FormatErrorMessage(string.Empty) };
// Add the ValidationResult's to the ModelState
foreach (var validationResultItem in validationResult)
bindingContext.ModelState.AddModelError(validationResultItem.Propertie, validationResultItem.ErrorMessage);
return model;
}
}
More Information
TypeDescriptor Class
System.ComponentModel.DataAnnotations Namespace
Thank you, thank you, dknaack!! Your answer was exactly what I was looking for, except I want to validate after each property is bound b/c I have properties that are dependent on other properties, and I don't want to continue binding if a dependent property is invalid.
Here's my new BindProperty overload:
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor){
// if this is a simple property, bind it and return
if(_simplePropertyKeys.ContainsKey(propertyDescriptor.Name)){
this.BindSimpleProperty(bindingContext, propertyDescriptor);
// if this is complex property, only bind it if we don't have an error already
} else if (bindingContext.ModelState.IsValid){
this.BindComplexProperty(bindingContext, propertyDescriptor);
}
// add errors from the data annotations
propertyDescriptor.Attributes.OfType<ValidationAttribute>()
.Where(a => a.IsValid(propertyDescriptor.GetValue(bindingContext.Model)) == false)
.ForEach(r => bindingContext.ModelState.AddModelError(propertyDescriptor.Name, r.ErrorMessage));
}
First of all, I'm only starting to learn ASP.NET so don't take my solution seriously. I found this article and as you, tried to do a custom model binder. There was no validation. Then i just replaced IModelBinder interface with DefaultModelBinder and voula, it works. Hope I could help someone

ViewModel with List<BaseClass> and editor templates

I have a view that lists tables being added to a floor plan. Tables derive from TableInputModel to allow for RectangleTableInputModel, CircleTableInputModel, etc
The ViewModel has a list of TableInputModel which are all one of the derived types.
I have a partial view for each of the derived types and given a List of mixed derived types the framework knows how to render them.
However, on submitting the form the type information is lost. I have tried with a custom model binder but because the type info is lost when it's being submitted, it wont work...
Has anyone tried this before?
Assuming you have the following models:
public abstract class TableInputModel
{
}
public class RectangleTableInputModel : TableInputModel
{
public string Foo { get; set; }
}
public class CircleTableInputModel : TableInputModel
{
public string Bar { get; set; }
}
And the following controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new TableInputModel[]
{
new RectangleTableInputModel(),
new CircleTableInputModel()
};
return View(model);
}
[HttpPost]
public ActionResult Index(TableInputModel[] model)
{
return View(model);
}
}
Now you could write views.
Main view Index.cshtml:
#model TableInputModel[]
#using (Html.BeginForm())
{
#Html.EditorForModel()
<input type="submit" value="OK" />
}
and the corresponding editor templates.
~/Views/Home/EditorTemplates/RectangleTableInputModel.cshtml:
#model RectangleTableInputModel
<h3>Rectangle</h3>
#Html.Hidden("ModelType", Model.GetType())
#Html.EditorFor(x => x.Foo)
~/Views/Home/EditorTemplates/CircleTableInputModel.cshtml:
#model CircleTableInputModel
<h3>Circle</h3>
#Html.Hidden("ModelType", Model.GetType())
#Html.EditorFor(x => x.Bar)
and final missing peace of the puzzle is the custom model binder for the TableInputModel type which will use the posted hidden field value to fetch the type and instantiate the proper implementation:
public class TableInputModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".ModelType");
var type = Type.GetType(
(string)typeValue.ConvertTo(typeof(string)),
true
);
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
which will be registered in Application_Start:
ModelBinders.Binders.Add(typeof(TableInputModel), new TableInputModelBinder());
and that's pretty much all. Now inside the Index Post action the model array will be properly initialzed with correct types.
There was "Derived Type Model Binder" in mvccontrib. But, unfortunately, there is no such binder in mvccontrib version 3

ASP.NET MVC UpdateModel with interface

I am trying to get UpdateModel to populate a model that is set as only an interface at compile-time. For example, I have:
// View Model
public class AccountViewModel {
public string Email { get; set; }
public IProfile Profile { get; set; }
}
// Interface
public interface IProfile {
// Empty
}
// Actual profile instance used
public class StandardProfile : IProfile {
public string FavoriteFood { get; set; }
public string FavoriteMusic { get; set; }
}
// Controller action
public ActionResult AddAccount(AccountViewModel viewModel) {
// viewModel is populated already
UpdateModel(viewModel.Profile, "Profile"); // This isn't working.
}
// Form
<form ... >
<input name='Email' />
<input name='Profile.FavoriteFood' />
<input name='Profile.FavoriteMusic' />
<button type='submit'></button>
</form>
Also note that I have a custom model binder that inherits from DefaultModelBinder being used that populates IProfile with an instance of StandardProfile in the overriden CreateModel method.
The problem is that FavoriteFood and FavoriteMusic are never populated. Any ideas? Ideally this would all be done in the model binder, but I'm not sure it is possible without writing a completely custom implementation.
Thanks, Brian
I would have to check the ASP.NET MVC code (DefaultModelBinder) but I'm guessing that its reflecting on the type IProfile, and not the instance, StandardProfile.
So it looks for any IProfile members it can try to bind, but its an empty interface, so it considers itself done.
You could try something like updating the BindingContext and changing the ModelType to StandardProfile and then calling
bindingContext.ModelType = typeof(StandardProfile);
IProfile profile = base.BindModel(controllerContext, bindingContext);
Anyways, having an empty Interface is weird~
Edit: just want to add that code above is just pseudo code, you would need to check DefaultModelBinder to see exactly what you want to write.
Edit#2:
Can you do:
public class ProfileModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
{
bindingContext.ModelType = typeof(StandardProfile);
return base.BindModel(controllerContext, bindingContext);
}
}
No need to make a model binder for AccountView, that one works fine.
Edit #3
Tested it out, the above binder works, just need to add:
ModelBinders.Binders[typeof(IProfile)] = new ProfileModelBinder();
Your action looks like:
public ActionResult AddAccount(AccountViewModel viewModel) {
// viewModel is fully populated, including profile, don't call UpdateModel
}
You can use IOC when setting the model binder (have the type constructor injected for instance).
Not inspecting the actual type behind the interface was discussed here: http://forums.asp.net/t/1348233.aspx
That said, I found a hackish way around the problem. Since I already had a custom model binder for this type, I was able to add some code to it to perform the binding for me. Here's what my model binder looks like now:
public class AccountViewModelModelBinder : DefaultModelBinder
{
private readonly IProfileViewModel profileViewModel;
private bool profileBound = false;
public AccountViewModelModelBinder(IProfileViewModel profileViewModel)
{
this.profileViewModel = profileViewModel;
}
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Bind the profile
if (profileBound)
return;
profileBound = true;
bindingContext.ModelType = profileViewModel.GetType();
bindingContext.Model = profileViewModel;
bindingContext.ModelName = "Profile";
BindModel(controllerContext, bindingContext);
}
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
var model = new AccountViewModel();
model.Profile = profileViewModel;
return model;
}
}
Basically, when the model binder is "done" binding the main AccountViewModel, I then alter the binding context (as suggested by eyston) and call BindModel once again. This then binds my profile. Note that I called GetType on the profileViewModel (which is supplied by the IOC container in the constructor). Also notice that I include a flag to indicate if the profile model has been bound already. Otherwise there would be an endless loop of OnModelUpdated being called.
I'm not saying this is pretty, but it does work well enough for my needs. I'd still love to hear about other suggestions.

Resources