I have a form with an input that collects a dollar amount (ex: $5,000)
How can I allow an input of $5,000 to not trigger (!ModelState.IsValid) remove comma (,) and $ for POSTing?
ViewModel
public class FormViewModel
{
[Required, RegularExpression("[0-9,]")]
public int? Amount { get; set; }
}
View
<form>
<input asp-for="Amount" />
<span asp-validation-for="Amount"></span>
<button type="submit">Submit</button>
</form>
Controller
[HttpPost]
public IActionResult PostForm(FormViewModel viewModel)
{
if (!ModelState.IsValid)
{
return View(viewModel)
}
else
{
//Post Form
}
}
I'd change the View Model to accept a string instead of int? for the Amount property:
public class FormViewModel {
public string Amount { get; set; }
public int? Value {
get {
int result = 0;
if (Int32.TryParse(Amount, NumberStyles.Currency, CultureInfo.CurrentCulture, result) {
return result;
}
return null;
}
}
}
Now user input is stored in the Amount property, while parsed integer value is stored in Value property.
Have you tried the IModelBinder?
public class DecimalModelBinder: System.Web.Mvc.IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
if (valueResult.AttemptedValue != string.Empty)
{
try
{
string attemptedVal = valueResult.AttemptedValue?.Replace(",", ".").Replace("$", "");
actualValue = Convert.ToDecimal(attemptedVal, new CultureInfo("en-US"));
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Itsnot the same but a similar answer to: error with decimal in mvc3 - the value is not valid for field
Update the model to use data type annotations.
public class FormViewModel {
[DataType(DataType.Currency)]
[Required]
public int? Amount { get; set; }
}
If you want to include cents then change property to decimal
public class FormViewModel {
[DataType(DataType.Currency)]
[Required]
public decimal? Amount { get; set; }
}
I ended up using AutoMapper to map the the properties back and forth.
I changed my ViewModel to a String and then used automapper like below:
//Map From Int To String
CreateMap<Models.ServiceModel, FormViewModel>().ForMember(dest => dest.Amount, opt => opt.MapFrom(src => src.Amount.ToString()));
//Map From String Back To Int
CreateMap<FormViewModel, Models.ServiceModel>().ForMember(dest => dest.Amount, opt => opt.MapFrom(src => int.Parse(src.Amount.Replace(",", ""))));
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 am updating database values using an action method but I am using ajax call to send updated values into that method so I am not binding model with this method so how can I validate the model for this action method such as I am not binding this with my method?
public ActionResult Update(int id, double? value)
{
if (!ModelState.IsValid)
{
Response.StatusCode = (int)HttpStatusCode.BadRequest;
return Json("Not valid model");
}
if (ModelState.IsValid) {
var oldTag = db.Tags.Where(x => x.Id == id).FirstOrDefault();
List<UpdatedData> updatedDatas = new List<UpdatedData> {
new UpdatedData
{
Id=id,
OldTagValue=oldTag.TagValue,
NewTagValue=value,
TagName = oldTag.TagName
}
};
obj.updatedDatas = new List<UpdatedData>();
obj.updatedDatas.AddRange(updatedDatas);
return PartialView("_Update_Confirmation", obj);
}
return View("Index");
}
you can change Update method
public class ProfileViewModel
{
[Required]
public int Id { get; set; }
public double? value { get; set; }
}
then
public ActionResult Update(ProfileViewModel viewModel)
{
if (!ModelState.IsValid)
{
Response.StatusCode = (int)HttpStatusCode.BadRequest;
return Json("Not valid model");
}
//...
}
The sample DataAnnotations model binder will fill model state with validation errors taken from the DataAnnotations attributes on your model.
I have a controller, view and model like below
//model
public class ItemViewModel
{
public string name {get;set;}
public decimal? TotalPrice { get; set; }
}
//controller
[HttpPost]
[MultipleButton(Name = "action", Argument = "Submit")]
public ActionResult Submit(ItemViewModel model)
{
decimal? a = model.TotalPrice;
Console.WriteLine(a);
}
//view
#Html.EditorFor(model => model.TotalPrice, new { htmlAttributes = new { #class = "form-control" } })
<input type="submit" name="action:Submit" class="btn btn-primary" value="Submit"/>
when I debug the model.TotalPrice is always null when there is a commas in number for example 10,234,776, but if there is no comma it gets the actual result. any idea?
//Change your ItemViewModel like
public class ItemViewModel
{
public string name { get; set; }
public string TotalPrice { get; set; }
}
//Introduce your custom Parser to handle the comma case
private bool MyTryParse(string toParse, out decimal result)
{
//Remove commas
string formatted = toParse.Replace(",", "");
return decimal.TryParse(formatted, out result);
}
//And your controller be like
[HttpPost]
[MultipleButton(Name = "action", Argument = "Submit")]
public ActionResult Submit(ItemViewModel model)
{
decimal a = null;
if (MyTryParse(model.TotalPrice, out a))
{
//Success code
}
else
{
//some error code
}
Console.WriteLine(a);
}
Note: Not maybe the best but a workable solution. Hope this helps your case.
Your code needs a decimal, the text box needs a string. The ViewModel is where you do the conversion.
private decimal totalPriceDecimal;
private string totalPriceString;
public string TotalPrice
{
get
{
return this.totalPriceString();
}
set
{
this.totalPriceString = value;
decimal temp;
if (int.TryParse(value, out temp))
{
this.totalPriceDecimal = temp;
}
}
}
Then your code that needs a decimal can use totalPriceDecimal, while you can bind the string to the text box.
More importantly, if the user enters something that can't be parsed, you know and can display an error message.
i tried it and it is working fine
the following is the code and the view assigned
[HttpPost]
public ActionResult Submit(ItemViewModel model)
{
decimal? a = model.TotalPrice;
Console.WriteLine(a);
return View();
}
public ActionResult Submit()
{
return View();
}
//=====================View============================
#model WebApplication4.Controllers.ItemViewModel
#Html.BeginForm()
{
#Html.EditorFor(model => model.TotalPrice, new { htmlAttributes = new { #class = "form-control" } })
}
In my MVC-4 application i take location from geolocation using javaScript and set latitude, longiture and accuracy in Hidden Fields. Values are set correctly in Hidden fields but during PostBack it shows null.
Here is my code :
Razor :
#using (Html.BeginForm()
{
#Html.HiddenFor(x => x.CurrentLocation.Latitude)
#Html.HiddenFor(x => x.CurrentLocation.Longitude)
#Html.HiddenFor(x => x.CurrentLocation.Accuracy)
<input type="submit" name="command" value="Start" />
}
JavaScript :
function SetLocation(position) {
console.log("{0},{1},{2}".format(position.coords.latitude, position.coords.longitude, position.coords.accuracy));
$("#CurrentLocation_Latitude").val(position.coords.latitude);
$("#CurrentLocation_Longitude").val(position.coords.longitude);
$("#CurrentLocation_Accuracy").val(position.coords.accuracy);
}
$(document).ready(function () {
$('form').submit(function () {
LocationService.getCurrentLocation(SetLocation);
});
});
But if I write this code then it`s work
$(document).ready(function () {
$('form').submit(function () {
$("#CurrentLocation_Latitude").val(100);
$("#CurrentLocation_Longitude").val(100);
});
});
This send proper value to the controller.
I don`t understand why this is happening.
Thank`s in advance.
Update 1 :
Model :
[ComplexType]
public class Location
{
public Location()
{
}
public Location(double latitude, double longiture, double? accuracy = null)
{
Latitude = latitude;
Longitude = longiture;
if (accuracy.HasValue)
Accuracy = accuracy.Value;
}
[DisplayName("Latitude : ")]
public double? Latitude { get; set; }
[DisplayName("Longitude : ")]
public double? Longitude { get; set; }
public double? Accuracy { get; set; }
}
public class ServiceInfoEditMetadata : ServiceInfo
{
public Int64 MachineId { get; set; }
[DisplayName("Client Name :")]
public string ClientName { get; set; }
[DisplayName("Site Name :")]
public string SiteName { get; set; }
public Location CurrentLocation { get; set; }
[DisplayName("Client Username :")]
public string ClientUsername { get; set; }
[DisplayName("Client Password :")]
public string ClientPassword { get; set; }
}
Controller :
public ActionResult Edit(Int64 id, ServiceInfoEditMetadata serviceInfoEditMetadata, string command)
{
try
{
switch (command)
{
case "Add":
AddMachineToServiceInfoDetails(serviceInfoEditMetadata);
return View(serviceInfoEditMetadata);
case "Start":
_serviceInfoService.StartService(serviceInfoEditMetadata, User.Identity.Name);
return RedirectToAction("Edit", serviceInfoEditMetadata.Id);
case "Update":
if (!ModelState.IsValid) return View(serviceInfoEditMetadata);
_serviceInfoService.UpdateServiceInfo(serviceInfoEditMetadata, User.Identity.Name);
return RedirectToAction("List");
}
return View(serviceInfoEditMetadata);
}
catch (Exception ex)
{
ModelState.AddModelError("ServiceInfoEditError", ex.Message);
return View(serviceInfoEditMetadata);
}
}
I assume your LocationService.getCurrentLocation is asynchronous. You should then delay form submission until it completes.
maybe you can try :
$(document).ready(function () {
var locationSet =false;
$('form').submit(function (event) {
if(!locationSet)
event.preventDefault();
LocationService.getCurrentLocation(
function(position){
SetLocation(position);
locationSet= true;
$('form').submit();
}
);
});
});
There could be more causes, because the mechanism (e.g mvc binding, ajax) of passing the fields' data is little complex.
I see that in the following js code you're passing handler to function 'SetLocation'. Try to invoke the function:
$(document).ready(function () {
$('form').submit(function () {
LocationService.getCurrentLocation(SetLocation(position));// 'SetLocation' with (position)
});
});
public class City
{
[DynamicReqularExpressionAttribute(PatternProperty = "RegEx")]
public string Zip {get; set;}
public string RegEx { get; set;}
}
I woud like to create this attribute where the pattern come from an other property and not declare static like in the original RegularExpressionAttribute.
Any ideas would be appreciated - Thanks
Something among the lines should fit the bill:
public class DynamicRegularExpressionAttribute : ValidationAttribute
{
public string PatternProperty { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo property = validationContext.ObjectType.GetProperty(PatternProperty);
if (property == null)
{
return new ValidationResult(string.Format("{0} is unknown property", PatternProperty));
}
var pattern = property.GetValue(validationContext.ObjectInstance, null) as string;
if (string.IsNullOrEmpty(pattern))
{
return new ValidationResult(string.Format("{0} must be a valid string regex", PatternProperty));
}
var str = value as string;
if (string.IsNullOrEmpty(str))
{
// We consider that an empty string is valid for this property
// Decorate with [Required] if this is not the case
return null;
}
var match = Regex.Match(str, pattern);
if (!match.Success)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
}
and then:
Model:
public class City
{
[DynamicRegularExpression(PatternProperty = "RegEx")]
public string Zip { get; set; }
public string RegEx { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var city = new City
{
RegEx = "[0-9]{5}"
};
return View(city);
}
[HttpPost]
public ActionResult Index(City city)
{
return View(city);
}
}
View:
#model City
#using (Html.BeginForm())
{
#Html.HiddenFor(x => x.RegEx)
#Html.LabelFor(x => x.Zip)
#Html.EditorFor(x => x.Zip)
#Html.ValidationMessageFor(x => x.Zip)
<input type="submit" value="OK" />
}
override the Validate method that takes the ValidationContext as a parameter, use the ValidationContext to get the regex string from the related property and apply the regex, returning the matched value.