ASP.NET MVC 3 - Choose which model property each submit button validates - asp.net

I have a view with three submit buttons. First button must validate some fields, second button must validate other fields, third button doesn't validate anything.
How can I do that on both client (unobtrusive) and server sides?

Most likely with alot of hand rolled usage, especially if you want it to be unobtrusive. You're going to need to create something like <input data-validationgroup="group1"... and then on the click action that your javascript code will then validate what you want. I would imagine jQuery Validate has some type of ruleset support but you'll have to figure that out.
You're going to have to do a similar sequence on the server side, and create ruleset type validation classes/blocks/methods that you interpret the submit action to the relevant ruleset. I'd look at a library like FluentValidation for this part.
To achieve what you want it, is is very extremely unlikely you will be able to achieve this using the DataAnnotations attributes on your model class.

Personally I have always liked and used the FluentValidation.NET library in all my projects. Not only that it is very powerful in terms of expressing validation rules but this library has an excellent integration with ASP.NET MVC. So I will try to provide a sample solution for this problem using it (only server side validation for the moment, later we can talk about unobtrusive client side validation if you want).
So start a new ASP.NET MVC 3 project using the default template and install the FluentValidation.MVC3 NuGet package (the current stable version is 2.0.0.0).
Then let's define a view model:
public class MyViewModel
{
public string Field1 { get; set; }
public string Field2 { get; set; }
}
Now we can assume that if button1 is clicked Field1 is required and if button2 is clicked Field2 is required and if button3 is clicked none of them are required. A fictional scenario but pretty close to your requirements.
Now let's define two different fluent validators for this model corresponding each to button1 and button2:
public class MyModelValidator1 : AbstractValidator<MyViewModel>
{
public MyModelValidator1()
{
RuleFor(x => x.Field1)
.NotEmpty();
}
}
public class MyModelValidator2 : AbstractValidator<MyViewModel>
{
public MyModelValidator2()
{
RuleFor(x => x.Field2)
.NotEmpty();
}
}
Now because it is only at runtime that we know which button is clicked we need to apply the correct validator based on value in the request. So let's write a custom validator provider factory:
public class MyFactory : IValidatorFactory
{
private readonly Func<HttpContextBase> _contextProvider;
public MyFactory(Func<HttpContextBase> contextProvider)
{
_contextProvider = contextProvider;
}
public IValidator GetValidator(Type type)
{
if (type == typeof(MyViewModel))
{
var context = _contextProvider();
if (!string.IsNullOrEmpty(context.Request["button1"]))
{
return new MyModelValidator1();
}
if (!string.IsNullOrEmpty(context.Request["button2"]))
{
return new MyModelValidator2();
}
}
return null;
}
public IValidator<T> GetValidator<T>()
{
return (IValidator<T>)GetValidator(typeof(T));
}
}
and register it in Application_Start:
ModelValidatorProviders.Providers.Add(
new FluentValidationModelValidatorProvider(
new MyFactory(() => new HttpContextWrapper(HttpContext.Current))
)
);
and that's pretty much all. Now what's left is trivial.
A controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
return Content("Thanks for submitting", "text/plain");
}
}
and a view:
#model MyViewModel
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(x => x.Field1)
#Html.EditorFor(x => x.Field1)
#Html.ValidationMessageFor(x => x.Field1)
</div>
<div>
#Html.LabelFor(x => x.Field2)
#Html.EditorFor(x => x.Field2)
#Html.ValidationMessageFor(x => x.Field2)
</div>
<input type="submit" value="Submit with button 1" name="button1" />
<input type="submit" value="Submit with button 2" name="button2" />
<input type="submit" value="Submit with button 3" name="button3" />
}

Related

MVC ViewModel returns ArgumentNullException

I am having a problem returning values to the controller when using a ViewModel.
For clarity I have simplified the code below where the original has many more fields.
When the page is loaded, the value in the hidden field is as expected. However when the form is submitted the value in the field is not being sent and instead I get an ArgumentNullException.
Please can you advise on what I am doing wrong.
View
#model Project.Models.SCView
#using (Html.BeginForm("ScorecardEdit"))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.FV.ID)
<input type="submit" value="Save" />
}
Model
public class FixView
{
public int ID { get; set; }
[DisplayFormat(DataFormatString = "{0:ddd dd/MM/yyyy}")]
public DateTime MatchDate { get; set; }
}
public class SCView
{
public FixView FV { get; set; }
public SCView()
{
this.FV = new FixView();
}
}
Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ScorecardEdit(SCView ReturnSC)
{
}
The code that you have should be working as MVC should be able to map your FV.ID property as expected as the HiddenFor() helper will generate the proper names to handle mapping it :
Using the same code that you provided, the debugger demonstrated the following after submitting the form :
The issue here sounds like you have a few other properties, possibly collection-based ones that use the DropDownListFor() helpers that have collections which are not posted to the server, so when you attempt to use the model you have to render populate one of those helpers, you are getting your ArgumentNullException.

Multiple buttons in the same form

I have one simple form with two buttons inside. Each button has to redirect me on different view in my controller. I was looking for some examples over the net, found solutions and implemented them. Unfortunately, it isn't working for me.
Controller:
public class HomeController : Controller
{
private MovieEntities db = new MovieEntities();
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost]
[Button(ButtonName = "clickButton", ButtonValue = "Send")]
public ActionResult Send()
{
return View();
}
[HttpPost]
[Button(ButtonName = "clickButton", ButtonValue = "Reset")]
public ActionResult Reset()
{
return View();
}
}
Index view:
#model IEnumerable<CustomWizzardMVC.Models.MovieInfo>
#{
ViewBag.Title = "Home";
}
<h1>Insert informations</h1>
#using(Html.BeginForm())
{
<input type="button" name="clickButton" value="Send" />
<input type="button" name="clickButton" value="Reset" />
}
Send and Reset view are just simple views with <p> tags inside.
I have Button class too:
public class Button : ActionNameSelectorAttribute
{
public string ButtonName { get; set; }
public string ButtonValue { get; set; }
public override bool IsValidName(ControllerContext controllerContext, string actionName, System.Reflection.MethodInfo methodInfo)
{
return controllerContext.HttpContext.Request[ButtonName] != null && controllerContext.HttpContext.Request[ButtonName] == ButtonValue;
}
}
What am I doing wrong in here? Also, if you know some other way to do the same functionality without using jQuery, please post some code :)
You can configure a form's target by it's action attribute.
So you can do this by changing your form's action attribute. You need to use client side script to do that.
An another option, you can send a value that contains user's option (like Option = "reset" or Option = "send"). And decide what view you need to go in your default view.
Change your input type="button" to type="submit.
<input type="button" /> buttons will not submit a form - they don't do anything by default. They're generally used in conjunction with JavaScript as part of an AJAX application.
<input type="submit"> buttons will submit the form they are in when the user clicks on them, unless you specify otherwise with JavaScript.
Found how it can be done. <input type="submit" value="Send" formaction="#Url.Action("Send","Home")" /> Just found out that formaction is the new HTML5 attribute that specifies the URL of the form for the HttpPost action. :)

Unobtrusive validation not working for custom validation attribute

I am trying to write a custom validation attribute in MVC and I can't make it work as an unobstructive validation. It works fine with postback (kinda) but as the form is on a dialog, I must have an Ajax style call or it's unusable. Maybe what i am trying to do is unachieveable. The problem is i need to connect to a database to do the check.
I made a simple demo for my problem.
My model
public class Customer
{
public int Id { get; set; }
[IsNameUnique]
[Required]
public string Name { get; set; }
}
The view:
#model WebApplication1.Models.Customer
#using (Html.BeginForm("Index", "Home", FormMethod.Post, new { #id = "NewForm" }))
{
#Html.ValidationSummary(true)
#Html.EditorFor(m => m.Name)
#Html.ValidationMessageFor(m => m.Name)
<br />
<input type="submit" value="Submit" />
}
Custom validation class
public class IsNameUnique : ValidationAttribute
{
private CustomerRepository _repository;
public IsNameUnique()
{
_repository = new CustomerRepository();
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if(value != null)
{
var isValid = _repository.IsNameUnique(value);
if(!isValid)
{
return new ValidationResult("Name must be unique");
}
}
return ValidationResult.Success;
}
}
Post method
[HttpPost]
public ActionResult Index(Customer customer)
{
if(ModelState.IsValid)
{
//add customer
}
return View();
}
database call
class CustomerRepository
{
internal bool IsNameUnique(object value)
{
//call to database
return false;
}
}
There is a form with a name field. I need to check if name is already in the database.
My question is how can I do unobtrusive style validation in my case? I found other posts on SO about IClientValidatable but none of them really show what I need to do. i.e. none of them do check against a database. Thanks.
Basically "unobtrusive validation" means "Client-Side validation, defined in an unobtrusive way". Key point here is "Client-Side", that is, validation which can be done via JavaScript in client browser.
Checking for name uniqueness involves server-side resources, and while it can be done in JavaScript using AJAX requests to server, usually people decide not to do so.
You can follow this guide for details of implementing unobtrusive validation: http://thewayofcode.wordpress.com/tag/custom-unobtrusive-validation/
In general you will need to do the following:
Enable unobtrusive validation in web.config
Include jQuery, jQuery Validate and unobtrusive scripts into your page
Implement IClientValidatable for your custom validation attribute
Implement and register client-side rules for your custom attribute
You may want to look into the [Remote] validation attribute. Just make a controller method that returns a JsonResult and map it to the remote attribute. This is probably the easiest way to accomplish what you're looking to do.
[Remote( "IsNameUnique", "Customers", HttpMethod = "post" )]
public override string Name { get; set; }
[HttpPost]
public JsonResult IsNameUnique( string name )
{
// Code
}
If you want to implement this as a custom validation, you need to do the following:
In your attribute, implement IClientValidatable. This requires you to implement GetClientValidationRules() method. Return a new client validation rule with your type and parameters.
Here's an example:
https://github.com/DustinEwers/dot-net-mvc-ui-demos/blob/master/ASPNET4/UIDemos/UIDemos/Attributes/PastDateOnlyAttribute.cs
Then you need to implement a jQuery validation rule. This is where you'd make your ajax call:
jQuery.validator.addMethod("pastdateonly", function (val, element, params) {
var value = $.trim($(element).val());
if (value === "") return true;
var maxDate = params.maxdate,
dteVal = new Date(value),
mxDte = new Date(maxDate);
return dteVal < mxDte;
});
Then add an unobtrusive adapter method.
jQuery.validator.unobtrusive.adapters.add("pastdateonly", ["maxdate"],
function (options) {
options.rules["pastdateonly"] = {
maxdate: options.params.maxdate
};
options.messages["pastdateonly"] = options.message;
}
);
Example:
https://github.com/DustinEwers/dot-net-mvc-ui-demos/blob/master/ASPNET4/UIDemos/UIDemos/Scripts/site.js

Passing partially populated Model to Create page without initializing other properties to default

ASP.NET MVC 5
I have a strongly typed create page.
My model is like this,
public class DemoViewModel
{
public int PackageId { get; set; }
public DateTime Date { get; set; }
}
my view is like this,
#model DemoViewModel
<div >
#Html.TextBoxFor(model => model.PackageId)
#Html.ValidationMessageFor(model => model.PackageId)
</div>
<div >
#Html.TextBoxFor(model => model.Date)
#Html.ValidationMessageFor(model => model.Date)
</div>
When the page is called, the PackageId is selected. So in my control I call the view like this,
public ActionResult Create()
{
return View(new DemoViewModel() {
PackageId = 1
});
}
The problem is that when I call like above the Date field get initialized to default and it shows "1/1/0001 12:00:00 AM" in the textbox. It is ok (that means empty when load) if I call the view without an model instance. But I need to pass some initial Model data to the view.
How can I pass an partially populated instance of mode without showing default values in other fields?
If you don't want your properties to be set to default value, you have to make them nullable by adding ?
So, in your case Date would be:
public DateTime? Date { get; set; }
More info here
You can create a wrapper method which takes as a parameter your model, binds it to the view, but excludes the Date property while binding, and then returns that view.
private GetCreateViewExcludingDate([Bind(Exclude="Date")]DemoViewModel demoVM)
{
return View("Create",demoVM);
}
And in your Create action method,
public ActionResult Create()
{
return GetCreateViewExcludingDate(new DemoViewModel() {
PackageId = 1
});
}
Of course, the other alternative is what #Mark said. Make your Date property nullable. But then you will have to check for null values in your controller whenever you use it.
EDIT:
As #sajith mentions in the comments, this solution DOES NOT WORK.
EDIT 2:
Here is an alternate solution. The textbox can be cleared on the client side using JavaScript/jQuery.
$(document).ready(function () {
$("input#Date").val("");
});
I tried this and it works fine.

Why is ASP.NET MVC 3 not validating my floats correctly?

I've got a View where I allow input, the fields are set as float in my SQL Server 2008 R2 database and I am using Entity Framwork 4.
In the Entity Framework Model the field looks like this private Nullable<global::System.Double> _TestNumber;
And the View uses an EditorField to allow input like this:
<div class="editor-field">
#Html.EditorFor(model => model.TestNumber)
#Html.ValidationMessageFor(model => model.TestNumber)
</div>
I am however getting this error in the Validation Message: The value '13.51' is not valid for TestNumber. I've tried with a comma instead of period, same thing.
Suggestions?
That should work:
View Model:
public class MyViewModel
{
public double? TestNumber { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel { TestNumber = 13.51 });
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
View:
#model AppName.Models.MyViewModel
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.TestNumber)
#Html.ValidationMessageFor(x => x.TestNumber)
<input type="submit" value="OK" />
}
One thing that you could checkout and which could explain the behavior you are observing is inconsistency between client side culture and server side culture. So for example if you have enabled client-side validation but the client culture uses , as decimal separator then 13.51 will fail client-side validation and if the server culture uses . as decimal separator then 13,51 would fail server side validation. So both 13.51 and 13,51 are failing to validate but on different layers. In order for the server to use the same culture as the client you could set the following the culture to auto in your web.config:
<globalization
requestEncoding="utf-8"
responseEncoding="utf-8"
culture="auto"
uiCulture="auto"
/>

Resources