I've been working on an MVC project that has a complex model with several nested classes, and one class has another class nested in it. I can get all of the other complex types to update correctly, but this last one never updates correctly. I've made sure to register its custom model binder, which gets executed and returns an object with the proper values assigned to its properties, but the original model never gets updated.
I've snipped out everything that works, leaving my structure only below:
Classes
public class Case
{
public Case()
{
PersonOfConcern = new Person();
}
public Person PersonOfConcern { get; set; }
}
[ModelBinder(typeof(PersonModelBinder))]
public class Person
{
public Person()
{
NameOfPerson = new ProperName();
}
public ProperName NameOfPerson { get; set; }
}
[TypeConverter(typeof(ProperNameConverter))]
public class ProperName : IComparable, IEquatable<string>
{
public ProperName()
: this(string.Empty)
{ }
public ProperName(string fullName)
{
/* snip */
}
public string FullName { get; set; }
}
Model Binder
public class PersonModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(Person))
{
HttpRequestBase request = controllerContext.HttpContext.Request;
string prefix = bindingContext.ModelName + ".";
if (request.Form.AllKeys.Contains(prefix + "NameOfPerson"))
{
return new Person()
{
NameOfPerson = new ProperName(request.Form.Get(prefix + "NameOfPerson"))
};
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
Controller
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
if (CurrentUser.HasAccess)
{
Case item = _caseData.Get(id);
if (TryUpdateModel(item, "Case", new string[] { /* other properties removed */ }, new string[] { "PersonOfConcern" })
&& TryUpdateModel(item.PersonOfConcern, "Case.PersonOfConcern"))
{
// ... Save here.
}
}
}
I'm at my wits' end. The PersonModelBinder gets executed and returns the correct set of values, but the model never gets updated. What am I missing here?
i think you should add it in global asax on Application_Start
ModelBinders.Binders.Add(typeof(PersonModelBinder ), new PersonModelBinder ());
Related
I have a Custom model binder that will convert posted values to another model.
Issue is bindingContext.ValueProvider.GetValue(modelName) returns none even if there are values posted from client.
Action Method
[HttpPost]
public ActionResult Update([DataSourceRequest] DataSourceRequest request,
[Bind(Prefix = "models")] AnotherModel items)
{
return Ok();
}
Target Model Class
[ModelBinder(BinderType = typeof(MyModelBinder))]
public class AnotherModel
{
IEnumerable<Dictionary<string, object>> Items { get; set; }
}
Cutomer Model Binder
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
// ISSUE: valueProviderResult is always None
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
//here i will convert valueProviderResult to AnotherModel
return Task.CompletedTask;
}
}
Quick watch shows ValueProvider does have values
UPDATE1
Inside the Update action method when i can iterate through IFormCollection, The Request.Form has all the Key and Value pair. Not sure why model binder is not able to retrieve it.
foreach (var f in HttpContext.Request.Form)
{
var key = f.Key;
var v = f.Value;
}
My example
In my client I send a header in request, this header is Base64String(Json Serialized object)
Object -> Json -> Base64.
Headers can't be multiline. With base64 we get 1 line.
All of this are applicable to Body and other sources.
Header class
public class RequestHeader : IHeader
{
[Required]
public PlatformType Platform { get; set; } //Windows / Android / Linux / MacOS / iOS
[Required]
public ApplicationType ApplicationType { get; set; }
[Required(AllowEmptyStrings = false)]
public string UserAgent { get; set; } = null!;
[Required(AllowEmptyStrings = false)]
public string ClientName { get; set; } = null!;
[Required(AllowEmptyStrings = false)]
public string ApplicationName { get; set; } = null!;
[Required(AllowEmptyStrings = true)]
public string Token { get; set; } = null!;
public string ToSerializedString()
{
return JsonConvert.SerializeObject(this);
}
}
IHeader Interface
public interface IHeader
{
}
Model Binder
public class HeaderParameterModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
StringValues headerValue = bindingContext.HttpContext.Request.Headers.Where(h =>
{
string guid = Guid.NewGuid().ToString();
return h.Key.Equals(bindingContext.ModelName ?? guid) |
h.Key.Equals(bindingContext.ModelType.Name ?? guid) |
h.Key.Equals(bindingContext.ModelMetadata.ParameterName);
}).Select(h => h.Value).FirstOrDefault();
if (headerValue.Any())
{
try
{
//Convert started
bindingContext.Model = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(headerValue)), bindingContext.ModelType);
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
}
catch
{
}
}
return Task.CompletedTask;
}
}
Model Binder Provider
We can work with any BindingSource.
Body
BindingSource Custom
BindingSource Form
BindingSource FormFile
BindingSource Header
BindingSource ModelBinding
BindingSource Path
BindingSource Query
BindingSource Services
BindingSource Special
public class ParametersModelBinderProvider : IModelBinderProvider
{
private readonly IConfiguration configuration;
public ParametersModelBinderProvider(IConfiguration configuration)
{
this.configuration = configuration;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType.GetInterfaces().Where(value => value.Name.Equals(nameof(ISecurityParameter))).Any() && BindingSource.Header.Equals(context.Metadata.BindingSource))
{
return new SecurityParameterModelBinder(configuration);
}
if (context.Metadata.ModelType.GetInterfaces().Where(value=>value.Name.Equals(nameof(IHeader))).Any() && BindingSource.Header.Equals(context.Metadata.BindingSource))
{
return new HeaderParameterModelBinder();
}
return null!;
}
}
In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0,new ParametersModelBinderProvider(configuration));
});
}
Controller action
ExchangeResult is my result class.
[HttpGet(nameof(Exchange))]
public ActionResult<ExchangeResult> Exchange([FromHeader(Name = nameof(RequestHeader))] RequestHeader header)
{
//RequestHeader previously was processed in modelbinder.
//RequestHeader is null or object instance.
//Some instructions
}
If you examine the source code of MVC's CollectionModelBinder, you'd notice that values of the form "name[index]" will return ValueProviderResult.None and need to be handled separately.
It seems like you're trying to solve the wrong problem. I'd suggest binding to a standard collection class like Dictionary.
Either;
public ActionResult Update([DataSourceRequest] DataSourceRequest request,
[Bind(Prefix = "models")] Dictionary<string, RecordTypeName> items)
Or;
public class AnotherModel : Dictionary<string, RecordTypeName> {}
If you don't know what type each dictionary value will have at compile time, that's where a custom binder would come in handy.
I have person model class defined as:
public class PersonModel
{
public bool SelectionSubmitted = false;
public bool ShowValidationSummary = false;
public string Name;
public string Get()
{
//actual implementation return some value from the db
return string.Empty;
}
}
The controller implementation is as follows:
class HomeController : Controller
{
[HttpGet]
public ActionResult Index(PersonModel model)
{
if (model.SelectionSubmitted && !ValidateSelections(model))
{
model.ShowValidationSummary = true;
}
return View("Index", model.Get());
}
private bool ValidateSelections(PersonModel model)
{
if(model.Name == "")
{
ModelState.AddModelError("EmptyPersonName", "Person name cannot be null");
}
return ModelState.IsValid;
}
}
The test class and method is defined as:
[TestClass]
public class ChildWithoutPlacementControllerTest
{
private readonly Mock<PersonModel> _mockPersonModel;
public ChildWithoutPlacementControllerTest()
{
_mockPersonModel = new Mock<PersonModel>();
}
[TestMethod]
public void GivenPerson_WhenSearchingForFutureBirthDate_ThenValidationMessageShouldBeShown()
{
//Arrange
HomeController controller = new HomeController();
_mockPersonModel.Setup(x => x.Get()).Returns(It.IsAny<string>());
_mockPersonModel.SetupGet(x => x.Name).Returns(string.Empty);
_mockPersonModel.SetupGet(x => x.SelectionSubmitted).Returns(true);
//Act
controller.Index(_mockPersonModel.Object);
//Assert
var isShowSummarySetToTrue = _mockPersonModel.Object.ShowValidationSummary;
Assert.IsTrue(isShowSummarySetToTrue);
}
}
What I want to achieve is mock the SelectionSubmitted and Name property to true and string.Empty respectively also Setup the Get method of PersonModel class, and check if the test return object has ShowValidationSummary set to true.
However, I am getting that I can't set up the non-virtual property Name.
Am I doing something wrong or is there any way to do it without changing the implementation code?
Am I doing something wrong
This appears to be an XY problem.
is there any way to do it without changing the implementation code
There really is no need for moq in this scenario. You can use inheritance to craft a fake model to be used in the test. The fake model will override the method that is tightly coupled to the database. (more on that later)
public class FakePerson : PersonModel {
public new string Get() {
return string.Empty; //Not calling the base Get
}
}
The test can then be refactored to use the fake model and be exercised to completion as intended.
[TestMethod]
public void GivenPerson_WhenSearchingForFutureBirthDate_ThenValidationMessageShouldBeShown() {
//Arrange
var fakePersonModel = new FakePerson() {
Name = string.Empty,
SelectionSubmitted = true
};
var controller = new HomeController();
//Act
controller.Index(fakePersonModel);
//Assert
var isShowSummarySetToTrue = fakePersonModel.ShowValidationSummary;
Assert.IsTrue(isShowSummarySetToTrue);
}
That aside, your model appears to be doing to much if the actual Get implementation does as you stated here
actual implementation return some value from the db
Consider refactoring that functionality out into a service (Single Responsibility Principle / Separation of Concerns)
public interface IPersonModelService {
string Get(PersonModel person);
}
public class PersonModelService : IPersonModelService {
public string Get(PersonModel person) {
//actual implementation return some value from the db
}
}
and keep the model as lean as possible. Also consider refactoring those public fields into public properties.
public class PersonModel {
public bool SelectionSubmitted { get; set; }
public bool ShowValidationSummary { get; set; }
public string Name { get; set; }
}
The controller would depend on the service abstraction
class HomeController : Controller {
private IPersonModelService service;
public HomeController(IPersonModelService service) {
this.service = service;
}
[HttpGet]
public ActionResult Index(PersonModel model) {
if (model.SelectionSubmitted && !ValidateSelections(model)) {
model.ShowValidationSummary = true;
}
return View("Index", service.Get(model));
}
private bool ValidateSelections(PersonModel model) {
if (model.Name == "") {
ModelState.AddModelError("EmptyPersonName", "Person name cannot be null");
}
return ModelState.IsValid;
}
}
And now the test can be exercised to completion in isolation.
[TestMethod]
public void GivenPerson_WhenSearchingForFutureBirthDate_ThenValidationMessageShouldBeShown() {
//Arrange
var model = new PersonModel() {
Name = string.Empty,
SelectionSubmitted = true
};
var serviceMock = new Mock<IPersonModelService>();
serviceMock.Setup(_ => _.Get(It.IsAny<PersonModel>())).Returns(string.Empty);
var controller = new HomeController(serviceMock.Object);
//Act
controller.Index(model);
//Assert
var isShowSummarySetToTrue = model.ShowValidationSummary;
Assert.IsTrue(isShowSummarySetToTrue);
}
I have the following model
public class Dog
{
public string NickName { get; set; }
public int Color { get; set; }
}
and I have the following api controller method which is exposed through an API
public class DogController : ApiController
{
// GET /v1/dogs
public IEnumerable<string> Get([FromUri] Dog dog)
{ ...}
Now, I would like to issue the GET request as follows:
GET http://localhost:90000/v1/dogs?nick_name=Fido&color=1
Question: How do I bind the query string parameter nick_name to property NickName in the dog class? I know I can call the API without the underscore (i.e. nickname) or change NickName to Nick_Name and get the value, but I need the names to remain like that for convention.
Edit
This question is not a duplicate because it is about ASP.NET WebApi not ASP.NET MVC 2
Implementing the IModelBinder,
public class DogModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(Dog))
{
return false;
}
var model = (Dog)bindingContext.Model ?? new Dog();
var hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
var searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";
model.NickName = GetValue(bindingContext, searchPrefix, "nick_name");
int colorId = 0;
if (int.TryParse(GetValue(bindingContext, searchPrefix, "colour"), out colorId))
{
model.Color = colorId; // <1>
}
bindingContext.Model = model;
return true;
}
private string GetValue(ModelBindingContext context, string prefix, string key)
{
var result = context.ValueProvider.GetValue(prefix + key); // <4>
return result == null ? null : result.AttemptedValue;
}
}
And Create ModelBinderProvider,
public class DogModelBinderProvider : ModelBinderProvider
{
private CollectionModelBinderProvider originalProvider = null;
public DogModelBinderProvider(CollectionModelBinderProvider originalProvider)
{
this.originalProvider = originalProvider;
}
public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
{
// get the default implementation of provider for handling collections
IModelBinder originalBinder = originalProvider.GetBinder(configuration, modelType);
if (originalBinder != null)
{
return new DogModelBinder();
}
return null;
}
}
and using in controller something like,
public IEnumerable<string> Get([ModelBinder(typeof(DogModelBinder))] Dog dog)
{
//controller logic
}
I'm posting json with variables names with underscores (like_this) and attempting to bind to a model that is camelcased (LikeThis), but the values are unable to be bound.
I know I could write a custom model binder, but since the underscored convention is so common I'd expect that a solution already existed.
The action/model I'm trying to post to is:
/* in controller */
[HttpPost]
public ActionResult UpdateArgLevel(UserArgLevelModel model) {
// do something with the data
}
/* model */
public class UserArgLevelModel {
public int Id { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public int ArgLevelId { get; set; }
}
and the json data is like:
{
id: 420007,
first_name: "Marc",
surname: "Priddes",
arg_level_id: 4
}
(Unfortunately I can't change either the naming of either the json or the model)
You can start writing a custom Json.NET ContractResolver:
public class DeliminatorSeparatedPropertyNamesContractResolver :
DefaultContractResolver
{
private readonly string _separator;
protected DeliminatorSeparatedPropertyNamesContractResolver(char separator)
: base(true)
{
_separator = separator.ToString();
}
protected override string ResolvePropertyName(string propertyName)
{
var parts = new List<string>();
var currentWord = new StringBuilder();
foreach (var c in propertyName)
{
if (char.IsUpper(c) && currentWord.Length > 0)
{
parts.Add(currentWord.ToString());
currentWord.Clear();
}
currentWord.Append(char.ToLower(c));
}
if (currentWord.Length > 0)
{
parts.Add(currentWord.ToString());
}
return string.Join(_separator, parts.ToArray());
}
}
This is for your particular case, becase you need a snake case ContractResolver:
public class SnakeCasePropertyNamesContractResolver :
DeliminatorSeparatedPropertyNamesContractResolver
{
public SnakeCasePropertyNamesContractResolver() : base('_') { }
}
Then you can write a custom attribute to decorate your controller actions:
public class JsonFilterAttribute : ActionFilterAttribute
{
public string Parameter { get; set; }
public Type JsonDataType { get; set; }
public JsonSerializerSettings Settings { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.ContentType.Contains("application/json"))
{
string inputContent;
using (var reader = new StreamReader(filterContext.HttpContext.Request.InputStream))
{
inputContent = reader.ReadToEnd();
}
var result = JsonConvert.DeserializeObject(inputContent, JsonDataType, Settings ?? new JsonSerializerSettings());
filterContext.ActionParameters[Parameter] = result;
}
}
}
And finally:
[JsonFilter(Parameter = "model", JsonDataType = typeof(UserArgLevelModel), Settings = new JsonSerializerSettings { ContractResolver = new SnakeCasePropertyNamesContractResolver() })]
public ActionResult UpdateArgLevel(UserArgLevelModel model) {
{
// model is deserialized correctly!
}
I have the following abstract class:
public abstract class TemplateBase
{
public abstract string TemplateName { get; }
public string RuntimeTypeName { get { return GetType().FullName; } }
public abstract List<AreaContainer> TemplateAreas { get; }
}
then these 2 inherited classes:
public class SingleColumnTemplate : TemplateBase
{
public override string TemplateName { get { return "Single column"; } }
public AreaContainer CenterColumn { get; private set; }
public SingleColumnTemplate()
{
this.CenterColumn = new AreaContainer("Middle");
}
private List<AreaContainer> templateAreas;
public override List<AreaContainer> TemplateAreas
{
get
{
if (this.templateAreas == null)
{
this.templateAreas = new List<AreaContainer>() { this.CenterColumn };
}
return this.templateAreas;
}
}
}
and
public class TwoColumnTemplate : TemplateBase
{
public override string TemplateName { get { return "Two column"; } }
public AreaContainer LeftColumn { get; private set; }
public AreaContainer RightColumn { get; private set; }
public TwoColumnTemplate()
{
LeftColumn = new AreaContainer("Left");
RightColumn = new AreaContainer("Right");
}
private List<AreaContainer> templateAreas;
public override List<AreaContainer> TemplateAreas
{
get
{
if (this.templateAreas == null)
{
this.templateAreas = new List<AreaContainer>() { this.LeftColumn, this.RightColumn };
}
return this.templateAreas;
}
}
}
I also have this class that is my model for editing:
public class ContentPage
{
public virtual int ContentPageId { get; set; }
public virtual string Title { get; set; }
public TemplateBase Template { get; set; }
}
Question:
for my ActionResults I have the following:
[HttpGet]
public ActionResult Edit()
{
var row = new ContentPage();
var template = new TwoColumnTemplate();
// Areas
HtmlArea html_left = new HtmlArea();
html_left.HtmlContent = "left area html content";
HtmlArea html_right = new HtmlArea();
html_right.HtmlContent = "right area html content";
template.LeftColumn.Areas.Add(html_left);
template.RightColumn.Areas.Add(html_right);
row.Template = template;
return View(row);
}
[HttpPost]
[ValidateInput(false)]
public ActionResult Edit(ContentPage row)
{
// Here i could loop through List -TemplateAreas and save each template Area to Db. I guess that would work
return this.View(row);
}
Question:
For HttpGet- how would I load row Template from the database? since it could be SingleColumnClass or TwoColumnClass.
how would my ViewModel look like to solve this?
thanks
You can write your own Model Binder that is responsible for binding TemplateBase. You will still need to have a way of knowing (in the model binder) which type you will be using a runtime, but you can always delegate that to a factory or service locator of some sort. I did a quick google search and here is a blog post I found that gives you some information for making a model binder for a similar scenario:
http://weblogs.asp.net/bhaskarghosh/archive/2009/07/08/7143564.aspx
EDIT: The blog leaves out how you tell MVC about your model binder. When the application starts, you can add your model binder to System.Web.Mvc.ModelBinders.Binders
HTH
You need to know the template type in you controller, so you can pass a parameter from the view to the controller, indicating the type (SingleColumn or TwoColumn). You could do this witn a Enum:
public enum TemplateType
{
SingleColumn,
TwoColumn
}
[HttpGet]
public ActionResult Edit(TemplateType templateType)
{
var row = new ContentPage();
TemplateBase template;
if (templateType == TemplateType.SingleColumn)
{
template = new SingleColumnTemplate();
}
else
{
template = new TwoColumnTemplate();
}
...
return View(row);
}
When you create the action link from your view you can specify:
<%= Html.ActionLink("Edit",
"Edit",
"YouController",
new
{
// singlecolumn or twocolumn
// depending on your concrete view
TemplateType = TemplateType.xxx
},
null);
I wonder if you could do something like this?
[HttpGet]
public ActionResult Edit(TemplateType templateType)
{
var row = new ContentPage();
TemplateBase template = (TemplateBase)Activator.CreateInstance(templateType);
...
return View(row);
}
templateType would have to be the exact name of your inherited classes (you can ignore case)