ASP.NET MVC Model Binding with Dashes in Form Element Names - c#

I have been scouring the internet trying to find a way to accomodate dashes from my form elements into the default model binding behavior of ASP.NET's Controllers in MVC 2, 3, or even 4.
As a front-end developer, I prefer dashes in my CSS over camelCase or underscores. In my markup, what I want to be able to do to is something like this:
<input type="text" name="first-name" class="required" />
<input type="text" name="last-name" class="required" />
In the controller, I would be passing in a C# object that would look like this:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
//etc...
}
Is there some way to extend the Controller class to accommodate this through some regex or other behavior? I hate the fact that I have to do something like this:
<input type="text" name="person.firstname" class="required" />
or even this:
<input type="text" name="isPersonAttending" class="required" />
Thoughts?

You could always create your own model binder.
Here's an example that implements a binder that supports adding Aliases to model properties:
http://ole.michelsen.dk/blog/bind-a-model-property-to-a-different-named-query-string-field/
And with it do something like:
[ModelBinder(typeof(AliasModelBinder))]
public class Person
{
[BindAlias("first-name")]
public string FirstName { get; set; }
[BindAlias("last-name")]
public string LastName { get; set; }
//etc...
}
EDIT:
This implementation, as the blogger says, is based on Andras' answer on the following SO question:
Asp.Net MVC 2 - Bind a model's property to a different named value

By creating a custom form value provider you could solve this problem easily. The other advantage is you can avoid polluting all the model properties by decorating custom attributes.
Custom Form Value Provider
public class DashFormValueProvider : NameValueCollectionValueProvider
{
public DashFormValueProvider(ControllerContext controllerContext)
: base(controllerContext.HttpContext.Request.Form,
controllerContext.HttpContext.Request.Unvalidated().Form,
CultureInfo.CurrentCulture)
{
}
public override bool ContainsPrefix(string prefix)
{
return base.ContainsPrefix(GetModifiedPrefix(prefix));
}
public override ValueProviderResult GetValue(string key)
{
return base.GetValue(GetModifiedPrefix(key));
}
public override ValueProviderResult GetValue(string key, bool skipValidation)
{
return base.GetValue(GetModifiedPrefix(key), skipValidation);
}
// this will convert the key "FirstName" to "first-name".
private string GetModifiedPrefix(string prefix)
{
return Regex.Replace(prefix, "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1-").ToLower();
}
}
Value Provider Factory
public class DashFormValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
return new DashFormValueProvider(controllerContext);
}
}
Global.asax.cs
ValueProviderFactories.Factories.Add(new DashFormValueProviderFactory());

For readers looking for an ASP.NET Core solution, try one of the [From...] attributes. For example:
public class Person
{
[FromForm(Name = "first-name")]
public string FirstName { get; set; }
[FromForm(Name = "last-name")]
public string LastName { get; set; }
//etc...
}
The [From...] attributes are briefly described at https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1#sources.

Related

ASP.NET MVC controller binding specific properties on child model with BindAttribute using Include

Prefix: I don't like this approach, I think that the Bind attribute is clunky. That said, I have a complex model. It is in an ASP.NET MVC razor view that uses a partial view.
Ideally I would like to just have the partial view accept a DTO that has only the fields that I would like to bind. For reasons I can't control I don't have that option.
However I have the ability to modify the controller. So, I am trying to use the Bind attribute to only allow the properties of the child that I would like to bind.
Here are the parent and child model classes:
public class Parent
{
public Menu FullMenu { get; set; }
public KidDTO SubDTO { get; set; }
}
public class KidDTO
{
public string Name { get; set;}
public string Date { get; set;}
public string Addr { get; set;}
}
Razor parent view (again I would prefer to only pass the smaller DTO that will be used... but I don't have that option):
#model Core.ViewModels.Parent
//Other stuff here
#{ Html.RenderPartial("_CreateEdit", Model);}
Here's the child view:
#model Core.ViewModels.Parent
#using (Ajax.BeginForm("CreateEdit", "Child", new AjaxOptions { HttpMethod = "POST"}))
{
<div class="form-horizontal">
<hr />
<div class="form-group row">
<div class="col-md-10">
#Html.EditorFor(model => model.SubDTO.Name)
</div>
</div>
<input type="button" value="Submit" />
</div>
}
Controller:
[HttpPost]
public async Task<ActionResult> CreateEdit([Bind(Include="SubDTO.Name")] Parent model)
{
// check if its Null or Not
var IsItNull = model.SubDTO.Name; //<--model.SubDTO.Name is always Null. ?
return RedirectToAction($"{ControllerEntity}Manager");
}
So, in short If I Bind to the SubDTO by iteslf it has values, but that then allows for binding to the other attributes of the SubModel, which is what I'm trying to avoid?
I was Not able to figure how to get the Include to work with Child Models properties. I could only bind the entire child or nothing.. However I was get it to work using the [Bind(Prefix="")] in the controller like so.
Controller.
[HttpPost]
public async Task<ActionResult> CreateEdit([Bind(Prefix="SubDTO.Name")] string childName)
{
// check if its Null or Not
var IsItNull = childName //<--UGLY but Appears to work, This is not Ideal.
return RedirectToAction($"{ControllerEntity}Manager");
}
Like the comments above alludes, I do not consider this to be the best solution for this common situation, but based on my restrictions it currently the only option that I have available. This can get very very ugly with a lot of properties.
Try changing your model to explicitly set what you can bind to at the class level.
public class Parent
{
public Menu FullMenu { get; set; }
public KidDTO SubDTO { get; set; }
}
[Bind(Include = "Name")]
public class KidDTO
{
public string Name { get; set;}
public string Date { get; set;}
public string Addr { get; set;}
}
Controller
[HttpPost]
public async Task<ActionResult> CreateEdit(Parent model)
{
// check if its Null or Not
var IsItNull = model.SubDTO.Name;
return RedirectToAction($"{ControllerEntity}Manager");
}
Form Collection Post
[HttpPost]
public async Task<ActionResult> CreateEdit(FormCollection collection)
{
var subDTO = new KidDTO()
{
Name = collection["SubDTO.Name"]
};
// check if its Null or Not
var IsItNull = subDTO.Name;
return RedirectToAction($"{ControllerEntity}Manager");
}
cshtml change type button to submit
<input type="submit" value="Submit" />
To keep things simple and clean, try creating a separate model just with the Name property. The drawback using explicit [Bind] is that you can now only bind to the Name using the default Model binder so why not just create a new ViewModel to begin with?

Exclude "The value 'null' is not valid for ..." in ActionFilterAttribute

In a webapi project we have a model like:
public class Person
{
public string Name { get; set; }
public Guid? Id { get; set; }
}
We have configured validation of parameters and we do some checks using ActionFilterAttribute:
public class ModelActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
(...)
var modelState = actionContext.ModelState;
if (modelState.IsValid == false)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
}
base.OnActionExecuting(actionContext);
}
}
The problem is that, doing a call like: https://localhost/person?Id=null&name='John', creates an error like:
The value 'null' is not valid for Id.
We have made the Id field nullable in the first place because we want to allow calls like the one above. Still, the validator complains. Is there any clean way to exclude this error?
I could go and iterate through the list of errors and exclude this one, but it feels really wrong.
You could define a model specific to the purpose. For example:
public class PersonSearchParameters
{
public string Name { get; set; }
public string Id { get; set; }
}
Then allow your method to handle parsing Id in the way you'd prefer to handle it.
I really think it'll be easier, though, if you just say that id should be omitted from your results if you want it to be null.

Posting part of viewModel to post action method

I would like to know if I am doing something wrong or it is not possible to post only part of view model using default model binder. Let's say I have a complex viewmodel that only small part it should be posted. I would like to achieve something like this:
public class ComplexViewModel
{
public object FirstNotPostedData { get; set; }
public object SecondNotPostedData { get; set; }
//......
public object NthNotPostedData { get; set; }
public InnerModelToPost InnerModelToPost { get; set; }
}
public class InnerModelToPost
{
public string FirstProperty { get; set; }
public string SecondProperty { get; set; }
public string ThirdProperty { get; set; }
}
In view I would like to display part of model and post the other part:
#model ComplexViewModel
#* DISPLAYING DATA *#
<form>
#Html.HiddenFor( m => m.InnerModelToPost.FirstProperty )
#Html.HiddenFor( m => m.InnerModelToPost.SecondProperty )
#Html.HiddenFor( m => m.InnerModelToPost.ThirdProperty )
<button type="submit">Submit button</button>
</form>
And then I would like to be able to pass this model to my controller in this way using default model binder:
public ActionResult GetOnlyImportantPartOfModel( InnerModelToPost innermodel)
{
//I'm getting empty model when I' doing like this
return View();
}
You may ask why not to pass entire model as parameter to this action method. So the answer is: code readablity. I store ComplexViewModel in session and I read it in first line of my action method. I would like to pass only this data that I want to update my model with.
Your need to use the Prefix property of [Bind] attribute to strip the InnerModelToPost prefix from your form values.
public ActionResult GetOnlyImportantPartOfModel([Bind(Prefix = "InnerModelToPost")] InnerModelToPost innermodel)
{
....
}
Having said that, if your only using properties of InnerModelToPost, then in your GET method, you can read the parent class (ComplexViewModel) from Session, but pass only the InnerModelToPost property to the view

Ordering attributes in ASP.NET WebApi response models inheriting href and id from a base class

I have an ASP.NET Web Api 2 project with several response models. In an attempt to create a smaller payload I am offering the user the option to collapse entities to just a id and a href link, which I would like to generate automatically. I would like all my main resource response models to inherit from a base response model that has just the href and id. If I have a resource Foo, this looks something like this:
public class ResourceResponseModel
{
public string Href { get; private set; }
public string Id { get; private set; }
protected ResourceResponseModel(string id)
{
Id = id;
}
}
public class FooModel : ResourceResponseModel
{
public string Name { get; private set; }
private ExampleModel (string id, string name)
: base(id)
{
Name = name;
}
internal static FooModel From(Foo foo)
{
return new FooModel(
foo.Id,
foo.Name
);
}
}
When my controller is called, this model is serialized with Microsoft.AspNet.Mvc.Json(object data)
This seems to work great, except when I look at the response I end up with, it puts the base class attributes at the end:
{
"name": "Foo 1",
"href": "api/abcdefg",
"id": "abcdefg"
}
Is there an easy way to get the base attributes to appear before the resource attributes?
You can solve this by setting the JsonProperty attribute on you properties and pass in Order.
public class ResourceResponseModel
{
[JsonProperty(Order = -2)]
public string Href { get; private set; }
[JsonProperty(Order = -2)]
public string Id { get; private set; }
protected ResourceResponseModel(string id)
{
Id = id;
}
}
Order seems to default to zero and then get sorted from low to high when serialized. Documentation can be found here.

C# MVC - Fetch name of property dynamically depending on model

In my MVC-project I have different custom validation-attributes. One of them is to check the value of a property against the value of another property.
As stated in many articles, I add something like
result.ValidationParameters.Add("otherproperty", _otherPropertyHtml);
result.ValidationParameters.Add("comparetype", _compareType);
result.ValidationParameters.Add("equalitytype", _equalityType);
to the returning ModelClientValidationRule object.
My problem now is, that - if my property to check - is encapsulated in another object, validation will not work.
If I create something like
#Html.TextBoxFor(m => m.ValueOne)
#Html.TextBoxFor(m => m.ValueTwo)
validation will work fine as it renders
data-val-otherproperty="ValueTwo"
My problem is for the following
#Html.TextBoxFor(m => m.IntermediateObject.ValueOne)
#Html.TextBoxFor(m => m.IntermediateObject.ValueTwo)
This will render two textboxes with names IntermediateObject_ValueOne and IntermediateObject.ValueTwo. But still data-val-otherproperty="ValueOne" for the first textbox.
How can it be achieved, that data-val-otherproperty has always the correct name of the other property?
My thoughts are something like HtmlHelper<>.NameFor(m => ...) or something that uses reflection?
Update 1 - Added code as requested by comments
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false)]
public class CustomCompareToOther : ValidationAttribute, IClientValidatable
{
// private backing-field
private readonly string _otherPropertyName;
// constructor
public OemCompareToOther(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
// implementation of IClientValidatable
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var result = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.DisplayName),
ValidationType = "customcomparetoother"
};
// add the property-name so it is known when rendered for client-side validation
result.ValidationParameters.Add("otherproperty", _otherPropertyHtml); // here I would need IntermediateObject.ValueTwo instead of only ValueTwo
yield return result;
}
}
Usage at model-level would be
public class MyModel
{
[CustomCompareToOther("ValueOTwo", CompareType.NotEqual, PropertyType.String)]
public string ValueOne { get; set; }
[CustomCompareToOther("ValueTwo", CompareType.NotEqual, PropertyType.String)]
public string ValueTwo { get; set; }
}
And what I will put into my View would be something like
public class ViewModel
{
public MyModel IntermediateObject { get; set; }
}
used e.g. return View(new ViewModel()).
So, in the rendered HTML I would have an input
<input type="text" name="IntermediateObject_ValueOne" id="IntermediateObject.ValueOne" data-val-customcomparetoother-otherpropertyname="ValueTwo" />
<input type="text" name="IntermediateObject_ValueTwo" id="IntermediateObject.ValueTwo" data-val-customcomparetoother-otherpropertyname="ValueOne" />
but I need
<input type="text" name="IntermediateObject_ValueOne" id="IntermediateObject.ValueOne" data-val-customcomparetoother-otherpropertyname="IntermediateObject.ValueTwo" />
<input type="text" name="IntermediateObject_ValueTwo" id="IntermediateObject.ValueTwo" data-val-customcomparetoother-otherpropertyname="IntermediateObject.ValueOne" />
in the html so javascript-validation can fetch the other property correctly.
You can use the [Compare("PropertyName")] Data Annotation.
Example in your View Model:
[Display(Name = "New Password")]
[DataType(DataType.Password)]
public string NewPassword { get; set; }
[Display(Name = "Confirm Password")]
[DataType(DataType.Password)]
[Compare("NewPassword")]
public string PasswordConfirmation { get; set; }
Just remember to add the System.ComponentModel.DataAnnotations namespace to your using statements

Categories