Is there a way to clear modelstate errors on the client? - asp.net-mvc

I have a model with a list of child objects. I have created a custom validation attribute, implemented IValidatableObject on the model and i get an error message as expected. The problem is that once the property has an error in the modelstate, i can't get the updated value to post back to the server. They get cleared out some time between hitting the submit button and receiving the model in the controller.
if i call ModelState.Clear() in the controller action, i don't get any messages but the new values post as expected. The model is however picking up on the custom attribute because ModelState.IsValid == false
I'm thinking the best way to handle this is to call ModelState.Clear() on the client somehow after $(ready) so i get the validation messages but can also have the changed values post to the server. Is this possible or is there a better way to do this?
Parent Model
public class PayrollPlanModel : IMapFrom<Data.PayrollPlan>
{
public int? PayrollPlanId { get; set; }
[Required]
public string Name { get; set; }
public List<PlanOptionFormModel> Options { get; set; }
}
Model List property on parent with custom attribute
public class PlanOptionFormModel : IValidatableObject
{
public int PlanOptionValueId { get; set; }
public int PayrollPlanId { get; set; }
public string PlanName { get; set; }
public int PlanOptionId { get; set; }
public string Description { get; set; }
[UIHint("_Money")]
[RequiredIf("Selected", true)]
public decimal? Value { get; set; }
public bool Selected { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Selected && !Value.HasValue)
{
yield return new ValidationResult("Add a value.");
}
}
}
Custom Attribute (Shamelessly stolen from here)
public class RequiredIfAttribute : ValidationAttribute
{
RequiredAttribute _innerAttribute = new RequiredAttribute();
public string _dependentProperty { get; set; }
public object _targetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue)
{
this._dependentProperty = dependentProperty;
this._targetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var field = validationContext.ObjectType.GetProperty(_dependentProperty);
if (field != null)
{
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
if ((dependentValue == null && _targetValue == null) || (dependentValue.Equals(_targetValue)))
{
if (!_innerAttribute.IsValid(value))
{
string name = validationContext.DisplayName;
return new ValidationResult(ErrorMessage = name + " Is required.");
}
}
return ValidationResult.Success;
}
else
{
return new ValidationResult(FormatErrorMessage(_dependentProperty));
}
}
}
Page snippet
for (int i = 0; i < Model.Options.Count; i++)
{
<div class="row">
<div class="col-md-3">
#Html.HiddenFor(m => Model.Options[i].PlanOptionValueId)
#Html.HiddenFor(m => Model.Options[i].PayrollPlanId)
#Html.HiddenFor(m => Model.Options[i].PlanOptionId)
#Html.HiddenFor(m => Model.Options[i].Description)
</div>
<div class="col-md-1 text-right">
#Html.CheckBoxFor(m => Model.Options[i].Selected, new { #data_textbox = "optionValue_" + i.ToString(), #class = "form-control modelOptionSelector" })
</div>
<div class="col-md-2 text-right">
<h4>#Model.Options[i].Description</h4>
</div>
<div class="col-md-1">
#Html.EditorFor(m => Model.Options[i].Value, Model.Options[i].Selected ? new { HtmlAttributes = new { id = "optionValue_" + i.ToString(), #class = "planOptionValueEditor" } } : (object)new { HtmlAttributes = new { disabled = "disabled", id = "optionValue_" + i.ToString(), #class = "planOptionValueEditor" } })
#Html.ValidationMessageFor(m => Model.Options[i].Value)
</div>
</div>
}
<br />
Editor Template
#model decimal?
#{
var defaultHtmlAttributesObject = new { };
var htmlAttributesObject = ViewData["htmlAttributes"] ?? new { };
var htmlAttributes = Html.MergeHtmlAttributes(htmlAttributesObject, defaultHtmlAttributesObject);
string attemptedValue = "";
ModelState modelStateForValue = Html.ViewData.ModelState[Html.IdForModel().ToString()];
if (modelStateForValue != null)
{
attemptedValue = modelStateForValue.Value.AttemptedValue;
}
}
#(Html.Kendo().CurrencyTextBoxFor(m => m)
.HtmlAttributes(htmlAttributes)
.Format("c")
.Spinners(false)
)
Controller
[HttpPost]
public ActionResult EditPlan(PayrollPlanModel model)
{
if(ModelState.IsValid)
{
}
else
{
}
return View(model);
}

It makes no sense to attempt to attempt to clear ModelState errors from the client. ModelState is only set within the controller method (by the DefaultModelBinder) when you make a request to the method. In any case, your issues are not related to ModelState being valid or invalid in the controller method.
There are a number of changes you need to make to your code:
You should delete your EditorTemplate for decimal? It means that any property of that type is going to use that template. Instead replace your
#Html.EditorFor(m => Model.Options[i].Value, ...)
with
#(Html.Kendo().CurrencyTextBoxFor(m => m.Options[i].Value)....
in the main view.
If you really do want to use a template, then make it a named template (which is called using #Html.EditorFor(m => Model.Options[i].Value, "yourTemplateName"), but in any case, you need to remove the code relating to attemptedValue and modelStateForValue (including the if block) - the EditorFor() methods will always correctly use values from ModelState if they exist.
Next, your RequiredIfAttribute does not implement IClientValidatable so you will not get client side validation. You could use the foolproof library, or if you want to write your own, refer this answer for the full implementation of a RequiredIfAttribute, including the scripts for client side validation.
Next, you need to delete the IValidatableObject implementation (the Validate() method) from your model. That is just repeating the validation that the [RequiredIf] attribute is doing, and you should avoid mixing ValidationAttribute's with IValidatableObject (refer The Complete Guide To Validation In ASP.NET MVC 3 - Part 2 for more detailed information).
Finally, the Kendo().CurrencyTextBoxFor() method hides the input and renders its own html. By default, hidden inputs are not validated, so you need to reconfigure the validator. In the main view, add the following script (after the jquery-{version}.js, jquery.validate.js and jquery.validate.unobtrusive.js scripts
<script>
$.validator.setDefaults({
ignore: []
});
.... // other scripts as required
<script>

Related

How to set default value to empty EditorFor-field when post a form i .NET MVC

I have a form in .NET MVC 5. where the user can write a number, default is "0", If the user deletes a number e.g. "233" an leaving the field empty. The form would not submit.
How can I submit the form with an empty field?
public class myModel
{
public int nummer { get; set; }
public myModel(){}
public myModel(int i) {this.nummer = i;}
}
razor code:
using (Html.BeginForm("myAction", "myController", FormMethod.Post, new { #class = "form-inline" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text- danger" })
#Html.EditorFor(model => model.nummer, new { htmlAttributes = new { #class = "form-control " } })
<input type="submit" value="submit" name="btnSubmit"/>
}
I am not interested in a validation error message, but to have the value set to "0", by default.
The DefaultModelBinder initializes your model using the parameterless constructor (your second constructor is never called). You will need to make the property nullable to prevent client and server side validation errors
public int? nummer { get; set; }
and then in the POST method, test for null and if so, set the value to 0
if(!model.nummer.HasValue)
{
model.nummer = 0;
}
Alternatively you could write your own ModelBinder that tests for a null value and in the ModelBinder, set the value to 0
You will need to set your nummer field to be a nullable type, like so:
public class myModel
{
public int? nummer { get; set; }
...
}
This will allow a user to submit the form without a value entered.
Then within your controller action you will need to assign a default value if the field is null:
if (model.nummer == null) model.nummer = 0;
Alternatively, you could use a private property like so:
public class myModel
{
private int? privateNummer { get; set; }
public int? nummer
{
get { return this.privateNummer == null ? 0 : this.privateNummer; }
set
{
this.privateNummer = value;
}
}
}
in your controller before passing the model back to the view do this ..
model.find(id);
model.nummer =0;
return View(model)

Passing model values to views MVC 5

I am in need of how the correct way to do this.
I can not use forms authentication
A user will "login" or confirm identity based on a value
I need to walk the user through a series of pages like so
Contact/MailAddress
Contact/Phone
Contact/Email
Contact/Summary
Questionaire/Question1
Questionaire/Question2
Questionaire/Question3
Questionaire/Summary
Final/Certify
Final/Review
I plan on using Session to hold the data but I'm having trouble figuring out how to pass the values to other views and how Redirect to other pages.
Any help will do...
Lets say you have some models like this
public class ContactModel
{
public string MailAddress { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
}
public class QuestionaireModel
{
public string Question1Answer { get; set; }
public string Question2Answer { get; set; }
public string Question3Answer { get; set; }
}
public class ContactQuestionaireModel
{
public ContactModel Contact { get; set; }
public QuestionaireModel Question { get; set; }
}
and you want to persist this model from view to view and action to action. In you controller you can create 2 actions. one for your first view and one for your second
Controller
public ActionResult ContactAddress()
{
var model = new ContactQuestionaireModel();
return View(model);
}
[HttpPost]
public ActionResult ContactAddress(ContactQuestionaireModel model)
{
var currentModel = TempData["currentModel"] as ContactQuestionaireModel;
currentModel.Contact.MailAddress = model.Contact.MailAddress;
TempData["currentModel"] = currentModel;
return RedirectToAction("ContactPhone");
}
public ActionResult ContactPhone()
{
var model = TempData["currentModel"] as ContactQuestionaireModel;
return View(model);
}
[HttpPost]
public ActionResult ContactPhone(ContactQuestionaireModel model)
{
var currentModel = TempData["currentModel"] as ContactQuestionaireModel;
currentModel.Contact.Phone = model.Contact.Phone;
TempData["currentModel"] = currentModel;
return RedirectToAction("ContactEmail");
}
in the first action ContactAddress you create a new blank model and pass that in to your view ContactAddress. In that view you can set TempData["currentModel"] equal to the model you are passing in. This will stay in TempData for 1 post back to the server. You dont need to do this on the first page since it's blank anyway but i'm doing it to save time.
View ContactAddress
#model WebApplication3.Models.ContactQuestionaireModel
#{
ViewBag.Title = "Contact Address";
TempData["currentModel"] = Model; //this will be available to me in the HttpPost action
}
#using (Html.BeginForm())
{
<div class="form-group">
#Html.LabelFor(m => m.Contact.MailAddress, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.TextBoxFor(m => m.Contact.MailAddress, new { #class = "form-control" })
</div>
</div>
<button type="submit">Submit</button>
}
you'll notice in the controller code that the Post Action for ContactAddress is setting a var currentModel equal to what is in TempData["currentModel"] which was set in the ContactAddress view. Before you do a redirect to the next action ContactPhone set TempData["currentModel"] back to the model you are building and use it in the next action.
You do have the option of adding the Model as a parameter to each action and passing the currentModel object like
public ActionResult ContactPhone(ContactQuestionaireModel model)
return RedirectToAction("ContactPhone", currentModel);
its up to you really. this is not a foolproof way. page refreshes and back and forward buttons could clear out everything that was entered. Storing the information in Session or actually saving the data in a database might be more optimal.
I advise against doing what you are attempting to do by logging in with session but what you are looking for is:
TempData.yourModel = new SomeModel { Data = "yourData" };
//next page
var model = (SomeModel)TempData.yourModel;
and
RedirectToAction("yourController", "yourAction");

MVC3 Validation - Require One From Group

Given the following viewmodel:
public class SomeViewModel
{
public bool IsA { get; set; }
public bool IsB { get; set; }
public bool IsC { get; set; }
//... other properties
}
I wish to create a custom attribute that validates at least one of the available properties are true. I envision being able to attach an attribute to a property and assign a group name like so:
public class SomeViewModel
{
[RequireAtLeastOneOfGroup("Group1")]
public bool IsA { get; set; }
[RequireAtLeastOneOfGroup("Group1")]
public bool IsB { get; set; }
[RequireAtLeastOneOfGroup("Group1")]
public bool IsC { get; set; }
//... other properties
[RequireAtLeastOneOfGroup("Group2")]
public bool IsY { get; set; }
[RequireAtLeastOneOfGroup("Group2")]
public bool IsZ { get; set; }
}
I would like to validate on the client-side prior to form submission as values in the form change which is why I prefer to avoid a class-level attribute if possible.
This would require both the server-side and client-side validation to locate all properties having identical group name values passed in as the parameter for the custom attribute. Is this possible? Any guidance is much appreciated.
Here's one way to proceed (there are other ways, I am just illustrating one that would match your view model as is):
[AttributeUsage(AttributeTargets.Property)]
public class RequireAtLeastOneOfGroupAttribute: ValidationAttribute, IClientValidatable
{
public RequireAtLeastOneOfGroupAttribute(string groupName)
{
ErrorMessage = string.Format("You must select at least one value from group \"{0}\"", groupName);
GroupName = groupName;
}
public string GroupName { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
foreach (var property in GetGroupProperties(validationContext.ObjectType))
{
var propertyValue = (bool)property.GetValue(validationContext.ObjectInstance, null);
if (propertyValue)
{
// at least one property is true in this group => the model is valid
return null;
}
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
private IEnumerable<PropertyInfo> GetGroupProperties(Type type)
{
return
from property in type.GetProperties()
where property.PropertyType == typeof(bool)
let attributes = property.GetCustomAttributes(typeof(RequireAtLeastOneOfGroupAttribute), false).OfType<RequireAtLeastOneOfGroupAttribute>()
where attributes.Count() > 0
from attribute in attributes
where attribute.GroupName == GroupName
select property;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var groupProperties = GetGroupProperties(metadata.ContainerType).Select(p => p.Name);
var rule = new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage
};
rule.ValidationType = string.Format("group", GroupName.ToLower());
rule.ValidationParameters["propertynames"] = string.Join(",", groupProperties);
yield return rule;
}
}
Now, let's define a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new SomeViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(SomeViewModel model)
{
return View(model);
}
}
and a view:
#model SomeViewModel
<script src="#Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.IsA)
#Html.ValidationMessageFor(x => x.IsA)
<br/>
#Html.EditorFor(x => x.IsB)<br/>
#Html.EditorFor(x => x.IsC)<br/>
#Html.EditorFor(x => x.IsY)
#Html.ValidationMessageFor(x => x.IsY)
<br/>
#Html.EditorFor(x => x.IsZ)<br/>
<input type="submit" value="OK" />
}
The last part that's left would be to register adapters for the client side validation:
jQuery.validator.unobtrusive.adapters.add(
'group',
[ 'propertynames' ],
function (options) {
options.rules['group'] = options.params;
options.messages['group'] = options.message;
}
);
jQuery.validator.addMethod('group', function (value, element, params) {
var properties = params.propertynames.split(',');
var isValid = false;
for (var i = 0; i < properties.length; i++) {
var property = properties[i];
if ($('#' + property).is(':checked')) {
isValid = true;
break;
}
}
return isValid;
}, '');
Based on your specific requirements the code might be adapted.
Use of require_from_group from jquery-validation team:
jQuery-validation project has a sub-folder in src folder called additional.
You can check it here.
In that folder we have a lot of additional validation methods that are not common that is why they're not added by default.
As you see in that folder it exists so many methods that you need to choose by picking which validation method you actually need.
Based on your question, the validation method you need is named require_from_group from additional folder.
Just download this associated file which is located here and put it into your Scripts application folder.
The documentation of this method explains this:
Lets you say "at least X inputs that match selector Y must be filled."
The end result is that neither of these inputs:
...will validate unless at least one of them is filled.
partnumber: {require_from_group: [1,".productinfo"]},
description: {require_from_group: [1,".productinfo"]}
options[0]: number of fields that must be filled in the group
options2: CSS selector that defines the group of conditionally required fields
Why you need to choose this implementation :
This validation method is generic and works for every input (text, checkbox, radio etc), textarea and select. This method also let you specify the minimum number of required inputs that need to be filled e.g
partnumber: {require_from_group: [2,".productinfo"]},
category: {require_from_group: [2,".productinfo"]},
description: {require_from_group: [2,".productinfo"]}
I created two classes RequireFromGroupAttribute and RequireFromGroupFieldAttribute that will help you on both server-side and client-side validations
RequireFromGroupAttribute class definition
RequireFromGroupAttribute only derives from Attribute. The class is use just for configuration e.g. setting the number of fields that need to be filled for the validation. You need to provide to this class the CSS selector class that will be used by the validation method to get all elements on the same group. Because the default number of required fields is 1 then this attribute is only used to decorate your model if the minimum requirement in the spcefied group is greater than the default number.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RequireFromGroupAttribute : Attribute
{
public const short DefaultNumber = 1;
public string Selector { get; set; }
public short Number { get; set; }
public RequireFromGroupAttribute(string selector)
{
this.Selector = selector;
this.Number = DefaultNumber;
}
public static short GetNumberOfRequiredFields(Type type, string selector)
{
var requiredFromGroupAttribute = type.GetCustomAttributes<RequireFromGroupAttribute>().SingleOrDefault(a => a.Selector == selector);
return requiredFromGroupAttribute?.Number ?? DefaultNumber;
}
}
RequireFromGroupFieldAttribute class definition
RequireFromGroupFieldAttribute which derives from ValidationAttribute and implements IClientValidatable. You need to use this class on each property in your model that participates to your group validation. You must pass the css selector class.
[AttributeUsage(AttributeTargets.Property)]
public class RequireFromGroupFieldAttribute : ValidationAttribute, IClientValidatable
{
public string Selector { get; }
public bool IncludeOthersFieldName { get; set; }
public RequireFromGroupFieldAttribute(string selector)
: base("Please fill at least {0} of these fields")
{
this.Selector = selector;
this.IncludeOthersFieldName = true;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var properties = this.GetInvolvedProperties(validationContext.ObjectType); ;
var numberOfRequiredFields = RequireFromGroupAttribute.GetNumberOfRequiredFields(validationContext.ObjectType, this.Selector);
var values = new List<object> { value };
var otherPropertiesValues = properties.Where(p => p.Key.Name != validationContext.MemberName)
.Select(p => p.Key.GetValue(validationContext.ObjectInstance));
values.AddRange(otherPropertiesValues);
if (values.Count(s => !string.IsNullOrWhiteSpace(Convert.ToString(s))) >= numberOfRequiredFields)
{
return ValidationResult.Success;
}
return new ValidationResult(this.GetErrorMessage(numberOfRequiredFields, properties.Values), new List<string> { validationContext.MemberName });
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var properties = this.GetInvolvedProperties(metadata.ContainerType);
var numberOfRequiredFields = RequireFromGroupAttribute.GetNumberOfRequiredFields(metadata.ContainerType, this.Selector);
var rule = new ModelClientValidationRule
{
ValidationType = "requirefromgroup",
ErrorMessage = this.GetErrorMessage(numberOfRequiredFields, properties.Values)
};
rule.ValidationParameters.Add("number", numberOfRequiredFields);
rule.ValidationParameters.Add("selector", this.Selector);
yield return rule;
}
private Dictionary<PropertyInfo, string> GetInvolvedProperties(Type type)
{
return type.GetProperties()
.Where(p => p.IsDefined(typeof(RequireFromGroupFieldAttribute)) &&
p.GetCustomAttribute<RequireFromGroupFieldAttribute>().Selector == this.Selector)
.ToDictionary(p => p, p => p.IsDefined(typeof(DisplayAttribute)) ? p.GetCustomAttribute<DisplayAttribute>().Name : p.Name);
}
private string GetErrorMessage(int numberOfRequiredFields, IEnumerable<string> properties)
{
var errorMessage = string.Format(this.ErrorMessageString, numberOfRequiredFields);
if (this.IncludeOthersFieldName)
{
errorMessage += ": " + string.Join(", ", properties);
}
return errorMessage;
}
}
How to use it in your view model?
In your model here is how to use it :
public class SomeViewModel
{
internal const string GroupOne = "Group1";
internal const string GroupTwo = "Group2";
[RequireFromGroupField(GroupOne)]
public bool IsA { get; set; }
[RequireFromGroupField(GroupOne)]
public bool IsB { get; set; }
[RequireFromGroupField(GroupOne)]
public bool IsC { get; set; }
//... other properties
[RequireFromGroupField(GroupTwo)]
public bool IsY { get; set; }
[RequireFromGroupField(GroupTwo)]
public bool IsZ { get; set; }
}
By default you don't need to decorate your model with RequireFromGroupAttribute because the default number of required fields is 1. But if you want a number of required fields to be different to 1 you can do the following :
[RequireFromGroup(GroupOne, Number = 2)]
public class SomeViewModel
{
//...
}
How to use it in your view code?
#model SomeViewModel
<script src="#Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/require_from_group.js")" type="text/javascript"></script>
#using (Html.BeginForm())
{
#Html.CheckBoxFor(x => x.IsA, new { #class="Group1"})<span>A</span>
#Html.ValidationMessageFor(x => x.IsA)
<br />
#Html.CheckBoxFor(x => x.IsB, new { #class = "Group1" }) <span>B</span><br />
#Html.CheckBoxFor(x => x.IsC, new { #class = "Group1" }) <span>C</span><br />
#Html.CheckBoxFor(x => x.IsY, new { #class = "Group2" }) <span>Y</span>
#Html.ValidationMessageFor(x => x.IsY)
<br />
#Html.CheckBoxFor(x => x.IsZ, new { #class = "Group2" })<span>Z</span><br />
<input type="submit" value="OK" />
}
Notice the group selector you specified when using RequireFromGroupField attribute is use in your view by specifing it as a class in each input involved in your groups.
That is all for the server side validation.
Let's talk about the client side validation.
If you check the GetClientValidationRules implementation in RequireFromGroupFieldAttribute class you will see I'm using the string requirefromgroup and not require_from_group as the name of method for the ValidationType property. That is because ASP.Net MVC only allows the name of the validation type to contain alphanumeric char and must not start with a number. So you need to add the following javascript :
$.validator.unobtrusive.adapters.add("requirefromgroup", ["number", "selector"], function (options) {
options.rules["require_from_group"] = [options.params.number, options.params.selector];
options.messages["require_from_group"] = options.message;
});
The javascript part is really simple because in the implementation of the adaptater function we just delegate the validation to the correct require_from_group method.
Because it works with every type of input, textarea and select elements, I may think this way is more generic.
Hope that helps!
I implemented Darin's awesome answer into my application, except I added it for strings and not boolean values. This was for stuff like name/company, or phone/email. I loved it except for one minor nitpick.
I tried to submit my form without a work phone, mobile phone, home phone, or email. I got four separate validation errors client side. This is fine by me because it lets the users know exactly what field(s) can be filled in to make the error go away.
I typed in an email address. Now the single validation under email went away, but the three remained under the phone numbers. These are also no longer errors anymore.
So, I reassigned the jQuery method that checks validation to account for this. Code below. Hope it helps someone.
jQuery.validator.prototype.check = function (element) {
var elements = [];
elements.push(element);
var names;
while (elements.length > 0) {
element = elements.pop();
element = this.validationTargetFor(this.clean(element));
var rules = $(element).rules();
if ((rules.group) && (rules.group.propertynames) && (!names)) {
names = rules.group.propertynames.split(",");
names.splice($.inArray(element.name, names), 1);
var name;
while (name = names.pop()) {
elements.push($("#" + name));
}
}
var dependencyMismatch = false;
var val = this.elementValue(element);
var result;
for (var method in rules) {
var rule = { method: method, parameters: rules[method] };
try {
result = $.validator.methods[method].call(this, val, element, rule.parameters);
// if a method indicates that the field is optional and therefore valid,
// don't mark it as valid when there are no other rules
if (result === "dependency-mismatch") {
dependencyMismatch = true;
continue;
}
dependencyMismatch = false;
if (result === "pending") {
this.toHide = this.toHide.not(this.errorsFor(element));
return;
}
if (!result) {
this.formatAndAdd(element, rule);
return false;
}
} catch (e) {
if (this.settings.debug && window.console) {
console.log("Exception occurred when checking element " + element.id + ", check the '" + rule.method + "' method.", e);
}
throw e;
}
}
if (dependencyMismatch) {
return;
}
if (this.objectLength(rules)) {
this.successList.push(element);
}
}
return true;
};
I know this is an old thread but I just came across the same scenario and found a few solutions and saw one that solves Matt's question above so I thought I would share for those who come across this answer. Check out: MVC3 unobtrusive validation group of inputs

multi-step registration process issues in asp.net mvc (split viewmodels, single model)

I have a multi-step registration process, backed by a single object in domain layer, which have validation rules defined on properties.
How should I validate the domain object when the domain is split across many views,
and I have to save the object partially in the first view when posted?
I thought about using Sessions but that's not possible cause the process is lengthy and amount of data is high, So I don't want to use session.
I thought about saving all the data in an relational in-memory db (with the same schema as main db) and then flushing that data to main db but issues arisen cause I should route between services (requested in the views) who work with the main db and in-memory db.
I'm looking for an elegant and clean solution (more precisely a best practice).
UPDATE AND Clarification:
#Darin Thank you for your thoughtful reply,
That was exactly what I've done till now.
But incidentally I've got a request which have many attachments in it, I design a Step2View e.g. which user can upload documents in it asynchronously ,
but those attachments should be saved in a table with referential relation to another table that should have been saved before in Step1View.
Thus I should save the domain object in Step1 (partially), But I can't,
cause the backed Core Domain object which is mapped partially to a Step1's ViewModel can't be saved without props that come from converted Step2ViewModel.
First you shouldn't be using any domain objects in your views. You should be using view models. Each view model will contain only the properties that are required by the given view as well as the validation attributes specific to this given view. So if you have 3 steps wizard this means that you will have 3 view models, one for each step:
public class Step1ViewModel
{
[Required]
public string SomeProperty { get; set; }
...
}
public class Step2ViewModel
{
[Required]
public string SomeOtherProperty { get; set; }
...
}
and so on. All those view models could be backed by a main wizard view model:
public class WizardViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
...
}
then you could have controller actions rendering each step of the wizard process and passing the main WizardViewModel to the view. When you are on the first step inside the controller action you could initialize the Step1 property. Then inside the view you would generate the form allowing the user to fill the properties about step 1. When the form is submitted the controller action will apply the validation rules for step 1 only:
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1
};
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step2", model);
}
Now inside the step 2 view you could use the Html.Serialize helper from MVC futures in order to serialize step 1 into a hidden field inside the form (sort of a ViewState if you wish):
#using (Html.BeginForm("Step2", "Wizard"))
{
#Html.Serialize("Step1", Model.Step1)
#Html.EditorFor(x => x.Step2)
...
}
and inside the POST action of step2:
[HttpPost]
public ActionResult Step2(Step2ViewModel step2, [Deserialize] Step1ViewModel step1)
{
var model = new WizardViewModel
{
Step1 = step1,
Step2 = step2
}
if (!ModelState.IsValid)
{
return View(model);
}
return View("Step3", model);
}
And so on until you get to the last step where you will have the WizardViewModel filled with all the data. Then you will map the view model to your domain model and pass it to the service layer for processing. The service layer might perform any validation rules itself and so on ...
There is also another alternative: using javascript and putting all on the same page. There are many jquery plugins out there that provide wizard functionality (Stepy is a nice one). It's basically a matter of showing and hiding divs on the client in which case you no longer need to worry about persisting state between the steps.
But no matter what solution you choose always use view models and perform the validation on those view models. As long you are sticking data annotation validation attributes on your domain models you will struggle very hard as domain models are not adapted to views.
UPDATE:
OK, due to the numerous comments I draw the conclusion that my answer was not clear. And I must agree. So let me try to further elaborate my example.
We could define an interface which all step view models should implement (it's just a marker interface):
public interface IStepViewModel
{
}
then we would define 3 steps for the wizard where each step would of course contain only the properties that it requires as well as the relevant validation attributes:
[Serializable]
public class Step1ViewModel: IStepViewModel
{
[Required]
public string Foo { get; set; }
}
[Serializable]
public class Step2ViewModel : IStepViewModel
{
public string Bar { get; set; }
}
[Serializable]
public class Step3ViewModel : IStepViewModel
{
[Required]
public string Baz { get; set; }
}
next we define the main wizard view model which consists of a list of steps and a current step index:
[Serializable]
public class WizardViewModel
{
public int CurrentStepIndex { get; set; }
public IList<IStepViewModel> Steps { get; set; }
public void Initialize()
{
Steps = typeof(IStepViewModel)
.Assembly
.GetTypes()
.Where(t => !t.IsAbstract && typeof(IStepViewModel).IsAssignableFrom(t))
.Select(t => (IStepViewModel)Activator.CreateInstance(t))
.ToList();
}
}
Then we move on to the controller:
public class WizardController : Controller
{
public ActionResult Index()
{
var wizard = new WizardViewModel();
wizard.Initialize();
return View(wizard);
}
[HttpPost]
public ActionResult Index(
[Deserialize] WizardViewModel wizard,
IStepViewModel step
)
{
wizard.Steps[wizard.CurrentStepIndex] = step;
if (ModelState.IsValid)
{
if (!string.IsNullOrEmpty(Request["next"]))
{
wizard.CurrentStepIndex++;
}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
wizard.CurrentStepIndex--;
}
else
{
// TODO: we have finished: all the step partial
// view models have passed validation => map them
// back to the domain model and do some processing with
// the results
return Content("thanks for filling this form", "text/plain");
}
}
else if (!string.IsNullOrEmpty(Request["prev"]))
{
// Even if validation failed we allow the user to
// navigate to previous steps
wizard.CurrentStepIndex--;
}
return View(wizard);
}
}
Couple of remarks about this controller:
The Index POST action uses the [Deserialize] attributes from the Microsoft Futures library so make sure you have installed the MvcContrib NuGet. That's the reason why view models should be decorated with the [Serializable] attribute
The Index POST action takes as argument an IStepViewModel interface so for this to make sense we need a custom model binder.
Here's the associated model binder:
public class StepViewModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var stepTypeValue = bindingContext.ValueProvider.GetValue("StepType");
var stepType = Type.GetType((string)stepTypeValue.ConvertTo(typeof(string)), true);
var step = Activator.CreateInstance(stepType);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => step, stepType);
return step;
}
}
This binder uses a special hidden field called StepType which will contain the concrete type of each step and which we will send on each request.
This model binder will be registered in Application_Start:
ModelBinders.Binders.Add(typeof(IStepViewModel), new StepViewModelBinder());
The last missing bit of the puzzle are the views. Here's the main ~/Views/Wizard/Index.cshtml view:
#using Microsoft.Web.Mvc
#model WizardViewModel
#{
var currentStep = Model.Steps[Model.CurrentStepIndex];
}
<h3>Step #(Model.CurrentStepIndex + 1) out of #Model.Steps.Count</h3>
#using (Html.BeginForm())
{
#Html.Serialize("wizard", Model)
#Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
#Html.EditorFor(x => currentStep, null, "")
if (Model.CurrentStepIndex > 0)
{
<input type="submit" value="Previous" name="prev" />
}
if (Model.CurrentStepIndex < Model.Steps.Count - 1)
{
<input type="submit" value="Next" name="next" />
}
else
{
<input type="submit" value="Finish" name="finish" />
}
}
And that's all you need to make this working. Of course if you wanted you could personalize the look and feel of some or all steps of the wizard by defining a custom editor template. For example let's do it for step 2. So we define a ~/Views/Wizard/EditorTemplates/Step2ViewModel.cshtml partial:
#model Step2ViewModel
Special Step 2
#Html.TextBoxFor(x => x.Bar)
Here's how the structure looks like:
Of course there is room for improvement. The Index POST action looks like s..t. There's too much code in it. A further simplification would involve into moving all the infrastructure stuff like index, current index management, copying of the current step into the wizard, ... into another model binder. So that finally we end up with:
[HttpPost]
public ActionResult Index(WizardViewModel wizard)
{
if (ModelState.IsValid)
{
// TODO: we have finished: all the step partial
// view models have passed validation => map them
// back to the domain model and do some processing with
// the results
return Content("thanks for filling this form", "text/plain");
}
return View(wizard);
}
which is more how POST actions should look like. I am leaving this improvement for the next time :-)
To supplement on Amit Bagga's answer you will find below what I did. Even if less elegant I find this way simpler than Darin's answer.
Controller :
public ActionResult Step1()
{
if (Session["wizard"] != null)
{
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
return View(wiz.Step1);
}
return View();
}
[HttpPost]
public ActionResult Step1(Step1ViewModel step1)
{
if (ModelState.IsValid)
{
WizardProductViewModel wiz = new WizardProductViewModel();
wiz.Step1 = step1;
//Store the wizard in session
Session["wizard"] = wiz;
return RedirectToAction("Step2");
}
return View(step1);
}
public ActionResult Step2()
{
if (Session["wizard"] != null)
{
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
return View(wiz.Step2);
}
return View();
}
[HttpPost]
public ActionResult Step2(Step2ViewModel step2)
{
if (ModelState.IsValid)
{
//Pull the wizard from session
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
wiz.Step2 = step2;
//Store the wizard in session
Session["wizard"] = wiz;
//return View("Step3");
return RedirectToAction("Step3");
}
return View(step2);
}
public ActionResult Step3()
{
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
return View(wiz.Step3);
}
[HttpPost]
public ActionResult Step3(Step3ViewModel step3)
{
if (ModelState.IsValid)
{
//Pull the wizard from session
WizardProductViewModel wiz = (WizardProductViewModel)Session["wizard"];
wiz.Step3 = step3;
//Save the data
Product product = new Product
{
//Binding with view models
Name = wiz.Step1.Name,
ListPrice = wiz.Step2.ListPrice,
DiscontinuedDate = wiz.Step3.DiscontinuedDate
};
db.Products.Add(product);
db.SaveChanges();
return RedirectToAction("Index", "Product");
}
return View(step3);
}
Models :
[Serializable]
public class Step1ViewModel
{
[Required]
[MaxLength(20, ErrorMessage="Longueur max de 20 caractères")]
public string Name { get; set; }
}
[Serializable]
public class Step2ViewModel
{
public Decimal ListPrice { get; set; }
}
[Serializable]
public class Step3ViewModel
{
public DateTime? DiscontinuedDate { get; set; }
}
[Serializable]
public class WizardProductViewModel
{
public Step1ViewModel Step1 { get; set; }
public Step2ViewModel Step2 { get; set; }
public Step3ViewModel Step3 { get; set; }
}
I would suggest you to maintain the state of Complete Process on the client using Jquery.
For Example we have a Three Step Wizard process.
The user in presented with the Step1 on which has a button Labeled "Next"
On Clicking Next We make an Ajax Request and Create a DIV called Step2 and load the HTML into that DIV.
On the Step3 we have a Button labeled "Finished" on Clicking on the button post the data using $.post call.
This way you can easily build your domain object directly from the form post data and in case the data has errors return valid JSON holding all the error message and display them in a div.
Please split the Steps
public class Wizard
{
public Step1 Step1 {get;set;}
public Step2 Step2 {get;set;}
public Step3 Step3 {get;set;}
}
public ActionResult Step1(Step1 step)
{
if(Model.IsValid)
{
Wizard wiz = new Wizard();
wiz.Step1 = step;
//Store the Wizard in Session;
//Return the action
}
}
public ActionResult Step2(Step2 step)
{
if(Model.IsValid)
{
//Pull the Wizard From Session
wiz.Step2=step;
}
}
The Above is just a demonstration which will help you achieve the end result. On the Final Step you have to create the Domain Object and populate the correct values from the Wizard Object and Store into the database.
I wanted to share my own way of handling these requirements. I did not want to use SessionState at all, nor did I want it handled client side, and the serialize method requires MVC Futures which I did not want to have to include in my project.
Instead I built an HTML Helper that will iterate through all of the properties of the model and generate a custom hidden element for each one. If it is a complex property then it will run recursively on it.
In your form they will be posted to the controller along with the new model data at each "wizard" step.
I wrote this for MVC 5.
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Routing;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Reflection;
namespace YourNamespace
{
public static class CHTML
{
public static MvcHtmlString HiddenClassFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
return HiddenClassFor(html, expression, null);
}
public static MvcHtmlString HiddenClassFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
{
ModelMetadata _metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if (_metaData.Model == null)
return MvcHtmlString.Empty;
RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;
return MvcHtmlString.Create(HiddenClassFor(html, expression, _metaData, _dict).ToString());
}
private static StringBuilder HiddenClassFor<TModel>(HtmlHelper<TModel> html, LambdaExpression expression, ModelMetadata metaData, IDictionary<string, object> htmlAttributes)
{
StringBuilder _sb = new StringBuilder();
foreach (ModelMetadata _prop in metaData.Properties)
{
Type _type = typeof(Func<,>).MakeGenericType(typeof(TModel), _prop.ModelType);
var _body = Expression.Property(expression.Body, _prop.PropertyName);
LambdaExpression _propExp = Expression.Lambda(_type, _body, expression.Parameters);
if (!_prop.IsComplexType)
{
string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(_propExp));
string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(_propExp));
object _value = _prop.Model;
_sb.Append(MinHiddenFor(_id, _name, _value, htmlAttributes));
}
else
{
if (_prop.ModelType.IsArray)
_sb.Append(HiddenArrayFor(html, _propExp, _prop, htmlAttributes));
else if (_prop.ModelType.IsClass)
_sb.Append(HiddenClassFor(html, _propExp, _prop, htmlAttributes));
else
throw new Exception(string.Format("Cannot handle complex property, {0}, of type, {1}.", _prop.PropertyName, _prop.ModelType));
}
}
return _sb;
}
public static MvcHtmlString HiddenArrayFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
return HiddenArrayFor(html, expression, null);
}
public static MvcHtmlString HiddenArrayFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
{
ModelMetadata _metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
if (_metaData.Model == null)
return MvcHtmlString.Empty;
RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;
return MvcHtmlString.Create(HiddenArrayFor(html, expression, _metaData, _dict).ToString());
}
private static StringBuilder HiddenArrayFor<TModel>(HtmlHelper<TModel> html, LambdaExpression expression, ModelMetadata metaData, IDictionary<string, object> htmlAttributes)
{
Type _eleType = metaData.ModelType.GetElementType();
Type _type = typeof(Func<,>).MakeGenericType(typeof(TModel), _eleType);
object[] _array = (object[])metaData.Model;
StringBuilder _sb = new StringBuilder();
for (int i = 0; i < _array.Length; i++)
{
var _body = Expression.ArrayIndex(expression.Body, Expression.Constant(i));
LambdaExpression _arrayExp = Expression.Lambda(_type, _body, expression.Parameters);
ModelMetadata _valueMeta = ModelMetadata.FromLambdaExpression((dynamic)_arrayExp, html.ViewData);
if (_eleType.IsClass)
{
_sb.Append(HiddenClassFor(html, _arrayExp, _valueMeta, htmlAttributes));
}
else
{
string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(_arrayExp));
string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(_arrayExp));
object _value = _valueMeta.Model;
_sb.Append(MinHiddenFor(_id, _name, _value, htmlAttributes));
}
}
return _sb;
}
public static MvcHtmlString MinHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
return MinHiddenFor(html, expression, null);
}
public static MvcHtmlString MinHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, object htmlAttributes)
{
string _id = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(expression));
string _name = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
object _value = ModelMetadata.FromLambdaExpression(expression, html.ViewData).Model;
RouteValueDictionary _dict = htmlAttributes != null ? new RouteValueDictionary(htmlAttributes) : null;
return MinHiddenFor(_id, _name, _value, _dict);
}
public static MvcHtmlString MinHiddenFor(string id, string name, object value, IDictionary<string, object> htmlAttributes)
{
TagBuilder _input = new TagBuilder("input");
_input.Attributes.Add("id", id);
_input.Attributes.Add("name", name);
_input.Attributes.Add("type", "hidden");
if (value != null)
{
_input.Attributes.Add("value", value.ToString());
}
if (htmlAttributes != null)
{
foreach (KeyValuePair<string, object> _pair in htmlAttributes)
{
_input.MergeAttribute(_pair.Key, _pair.Value.ToString(), true);
}
}
return new MvcHtmlString(_input.ToString(TagRenderMode.SelfClosing));
}
}
}
Now for all steps of your "wizard" you can use the same base model and pass the "Step 1,2,3" model properties into the #Html.HiddenClassFor helper using a lambda expression.
You can even have a back button at each step if you want to. Just have a back button in your form that will post it to a StepNBack action on the controller using the formaction attribute.
Not included in the below example but just an idea for you.
Anyways here is a basic example:
Here is your MODEL
public class WizardModel
{
// you can store additional properties for your "wizard" / parent model here
// these properties can be saved between pages by storing them in the form using #Html.MinHiddenFor(m => m.WizardID)
public int? WizardID { get; set; }
public string WizardType { get; set; }
[Required]
public Step1 Step1 { get; set; }
[Required]
public Step2 Step2 { get; set; }
[Required]
public Step3 Step3 { get; set; }
// if you want to use the same model / view / controller for EDITING existing data as well as submitting NEW data here is an example of how to handle it
public bool IsNew
{
get
{
return WizardID.HasValue;
}
}
}
public class Step1
{
[Required]
[MaxLength(32)]
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Required]
[MaxLength(32)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
}
public class Step2
{
[Required]
[MaxLength(512)]
[Display(Name = "Biography")]
public string Biography { get; set; }
}
public class Step3
{
// lets have an array of strings here to shake things up
[Required]
[Display(Name = "Your Favorite Foods")]
public string[] FavoriteFoods { get; set; }
}
Here is your CONTROLLER
public class WizardController : Controller
{
[HttpGet]
[Route("wizard/new")]
public ActionResult New()
{
WizardModel _model = new WizardModel()
{
WizardID = null,
WizardType = "UserInfo"
};
return View("Step1", _model);
}
[HttpGet]
[Route("wizard/edit/{wizardID:int}")]
public ActionResult Edit(int wizardID)
{
WizardModel _model = database.GetData(wizardID);
return View("Step1", _model);
}
[HttpPost]
[Route("wizard/step1")]
public ActionResult Step1(WizardModel model)
{
// just check if the values in the step1 model are valid
// shouldn't use ModelState.IsValid here because that would check step2 & step3.
// which isn't entered yet
if (ModelState.IsValidField("Step1"))
{
return View("Step2", model);
}
return View("Step1", model);
}
[HttpPost]
[Route("wizard/step2")]
public ActionResult Step2(WizardModel model)
{
if (ModelState.IsValidField("Step2"))
{
return View("Step3", model);
}
return View("Step2", model);
}
[HttpPost]
[Route("wizard/step3")]
public ActionResult Step3(WizardModel model)
{
// all of the data for the wizard model is complete.
// so now we check the entire model state
if (ModelState.IsValid)
{
// validation succeeded. save the data from the model.
// the model.IsNew is just if you want users to be able to
// edit their existing data.
if (model.IsNew)
database.NewData(model);
else
database.EditData(model);
return RedirectToAction("Success");
}
return View("Step3", model);
}
}
Here are your VIEWS
Step 1
#model WizardModel
#{
ViewBag.Title = "Step 1";
}
#using (Html.BeginForm("Step1", "Wizard", FormMethod.Post))
{
#Html.MinHiddenFor(m => m.WizardID)
#Html.MinHiddenFor(m => m.WizardType)
#Html.LabelFor(m => m.Step1.FirstName)
#Html.TextBoxFor(m => m.Step1.FirstName)
#Html.LabelFor(m => m.Step1.LastName)
#Html.TextBoxFor(m => m.Step1.LastName)
<button type="submit">Submit</button>
}
Step 2
#model WizardModel
#{
ViewBag.Title = "Step 2";
}
#using (Html.BeginForm("Step2", "Wizard", FormMethod.Post))
{
#Html.MinHiddenFor(m => m.WizardID)
#Html.MinHiddenFor(m => m.WizardType)
#Html.HiddenClassFor(m => m.Step1)
#Html.LabelFor(m => m.Step2.Biography)
#Html.TextAreaFor(m => m.Step2.Biography)
<button type="submit">Submit</button>
}
Step 3
#model WizardModel
#{
ViewBag.Title = "Step 3";
}
#using (Html.BeginForm("Step3", "Wizard", FormMethod.Post))
{
#Html.MinHiddenFor(m => m.WizardID)
#Html.MinHiddenFor(m => m.WizardType)
#Html.HiddenClassFor(m => m.Step1)
#Html.HiddenClassFor(m => m.Step2)
#Html.LabelFor(m => m.Step3.FavoriteFoods)
#Html.ListBoxFor(m => m.Step3.FavoriteFoods,
new SelectListItem[]
{
new SelectListItem() { Value = "Pizza", Text = "Pizza" },
new SelectListItem() { Value = "Sandwiches", Text = "Sandwiches" },
new SelectListItem() { Value = "Burgers", Text = "Burgers" },
});
<button type="submit">Submit</button>
}
Wizards are just simple steps in processing a simple model. There is no reason to create multiple models for a wizard. All you would do is create a single model and pass it between actions in a single controller.
public class MyModel
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set };
public string StepOneData { get; set; }
public string StepTwoData { get; set; }
}
The above coed is stupid simple so replace your fields in there. Next we start with a simple action that initiates our wizard.
public ActionResult WizardStep1()
{
return View(new MyModel());
}
This calls the view "WizardStep1.cshtml (if using razor that is). You can use the create template wizard if you want. We will just be redirecting the post to a different action.
<WizardStep1.cshtml>
#using (Html.BeginForm("WizardStep2", "MyWizard")) {
The thing of note is that we will be posting this to a different action; the WizardStep2 action
[HttpPost]
public ActionResult WizardStep2(MyModel myModel)
{
return ModelState.IsValid ? View(myModel) : View("WizardStep1", myModel);
}
In this action we check if our model is valid, and if so we send it to our WizardStep2.cshtml view else we send it back to step one with the validation errors. In each step we send it to the next step, validate that step and move on. Now some savvy developers might say well we can't move between steps such as this if we use [Required] attributes or other data annotations between steps. And you would be right, so remove the errors on items that are yet to be checked. like below.
[HttpPost]
public ActionResult WizardStep3(MyModel myModel)
{
foreach (var error in ModelState["StepTwoData"].Errors)
{
ModelState["StepTwoData"].Errors.Remove(error);
}
Finally we would save the model once to the data store. This also prevents a user that starts a wizard but doesn't finish it not to save incomplete data to the database.
I hope you find this method of implementing a wizard much easier to use and maintain than any of the previously mentioned methods.
Thanks for reading.
Adding more info from #Darin's answer.
What if you have separate design style for each steps and wanted maintain each in separate partial view or what if you have multiple properties for each step ?
While using Html.EditorFor we have limitation to use partial view.
Create 3 Partial Views under Shared folder named : Step1ViewModel.cshtml , Step3ViewModel.cshtml , Step3ViewModel.cshtml
For brevity I just posting 1st patial view, other steps are same as Darin's answer.
Step1ViewModel.cs
[Serializable]
public class Step1ViewModel : IStepViewModel
{
[Required]
public string FirstName { get; set; }
public string LastName { get; set; }
public string PhoneNo { get; set; }
public string EmailId { get; set; }
public int Age { get; set; }
}
Step1ViewModel.cshtml
#model WizardPages.ViewModels.Step1ViewModel
<div class="container">
<h2>Personal Details</h2>
<div class="form-group">
<label class="control-label col-sm-2" for="email">First Name:</label>
<div class="col-sm-10">
#Html.TextBoxFor(x => x.FirstName)
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="pwd">Last Name:</label>
<div class="col-sm-10">
#Html.TextBoxFor(x => x.LastName)
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="pwd">Phone No:</label>
<div class="col-sm-10">
#Html.TextBoxFor(x => x.PhoneNo)
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" for="pwd">Email Id:</label>
<div class="col-sm-10">
#Html.TextBoxFor(x => x.EmailId)
</div>
</div>
</div>
Index.cshtml
#using Microsoft.Web.Mvc
#model WizardPages.ViewModels.WizardViewModel
#{
var currentStep = Model.Steps[Model.CurrentStepIndex];
string viewName = currentStep.ToString().Substring(currentStep.ToString().LastIndexOf('.') + 1);
}
<h3>Step #(Model.CurrentStepIndex + 1) out of #Model.Steps.Count</h3>
#using (Html.BeginForm())
{
#Html.Serialize("wizard", Model)
#Html.Hidden("StepType", Model.Steps[Model.CurrentStepIndex].GetType())
#Html.Partial(""+ viewName + "", currentStep);
if (Model.CurrentStepIndex > 0)
{
<input type="submit" value="Previous" name="prev" class="btn btn-warning" />
}
if (Model.CurrentStepIndex < Model.Steps.Count - 1)
{
<input type="submit" value="Next" name="next" class="btn btn-info" />
}
else
{
<input type="submit" value="Finish" name="finish" class="btn btn-success" />
}
}
If there some better solution, please comment to let others know.
One option is to create set of identical tables that will store the data collected in each step. Then in the last step if all goes well you can create the real entity by copying the temporary data and store it.
Other is to create Value Objects for each step and store then in Cache or Session. Then if all goes well you can create your Domain object from them and save it

EF 4 CTP 5 saving a many-to-many navigation property

I'm using EF4 CTP5 and am having trouble saving records back to the database. I have Contact and ContactType entities. As the post title states, I have set up a many-to-many navigation property between the tables.
The problem is with validating the ContactType values. ModelState.IsValid is false because it's unable to convert the values passed back from the form (a string array of ContactType id's into ContactType objects.
POCO's
public partial class Contact
{
public Contact()
{
this.ContactTypes = new HashSet<ContactType>();
}
// Primitive properties
public int ContactId { get; set; }
public string ContactName { get; set; }
// Navigation properties
public virtual ICollection<ContactType> ContactTypes { get; set; }
}
public partial class ContactType
{
public ContactType()
{
this.Contacts = new HashSet<Contact>();
}
// Primitive properties
public int ContactTypeId { get; set; }
public string Name { get; set; }
// Navigation properties
public virtual ICollection<Contact> Contacts { get; set; }
}
Controller
//
// GET: /Contact/Edit/5
public virtual ActionResult Edit(int id)
{
Contact contact = context.Contacts.Include(c => c.ContactTypes).Single(x => x.ContactId == id);
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
//
// POST: /Contact/Edit/5
[HttpPost]
public virtual ActionResult Edit(Contact contact)
{
if (ModelState.IsValid)
{
context.Entry(contact).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
View
<div class="field-block">
#Html.LabelFor(model => model.ContactId)
#Html.EditorFor(model => model.ContactId, new { fieldName = "ContactId" })
#Html.ValidationMessageFor(model => model.ContactId)
</div>
<div class="field-block">
#Html.LabelFor(model => model.OrganizationNameInternal)
#Html.EditorFor(model => model.OrganizationNameInternal)
#Html.ValidationMessageFor(model => model.OrganizationNameInternal)
</div>
<div class="field-block">
#Html.LabelFor(model => model.ContactTypes)
#Html.ListBoxFor(modelContactType,
new MultiSelectList((IEnumerable<TDAMISObjects.ContactType>)ViewData["ContactTypesAll"],
"ContactTypeId",
"Name",
Model.ContactTypes))
#Html.ValidationMessageFor(model => model.ContactTypes)
</div>
ModelState error
ModelState.Values.ElementAt(2).Value
{System.Web.Mvc.ValueProviderResult}
AttemptedValue: "5"
Culture: {en-US}
RawValue: {string[1]}
ModelState.Values.ElementAt(2).Errors[0]
{System.Web.Mvc.ModelError}
ErrorMessage: ""
Exception: {"The parameter conversion from type 'System.String' to type 'ProjectObjects.ContactType' failed because no type converter can convert between these types."}
So it seems pretty clear what the problem is, but I can't seem to find the solution. I have tried to manually convert the ContactType id's into ContactType objects, and adding them to the Contact object passed into the Edit function (called 'contact'):
contact.ContactTypes.Clear();
string[] ids = this.HttpContext.Request.Form["ContactTypes"].Split(',');
for(int i = 0; i< ids.Length; i++)
{
int x = Convert.ToInt32(ids[i]);
ContactType selectedType = context.ContactTypes.Single(t => t.ContactTypeId == x);
contact.ContactTypes.Add(selectedType);
}
but the error persists. I've also tried calling
context.ChangeTracker.DetectChanges();
but that did not do the trick. I also manually set the ValueProviderResult for the value that will not validate, using
ModelState.SetModelValue("ContactTypes", val);
Which also did not work. I feel like I'm missing something basic here. Any ideas?
Thanks, Steve
Well after more work on this, I found a work around. Basically, I had to ignore the validation errors, then manually remove existing ContactTypes, then add in ones selected by the user. I did try to build a custom validator for the Contact.ContactTypes propery, but a ContactType object was always passed in to that method; I never saw the array of strings. Admittedly, that was the first custom validator I have built, so perhaps I was missing something.
Anyway, here's the Edit method I ended up with:
//
// POST: /Contact/Edit/5
[HttpPost]
public virtual ActionResult Edit(Contact contact)
{
// clear up ModelState.IsValid for contact type
ModelState.Remove("ContactTypes");
if(ModelState.IsValid)
{
// remove all contact types for contact
Contact dummy = context.Contacts.Single(c => c.ContactId == contact.ContactId);
if(dummy.ContactTypes.Count > 0)
{
dummy.ContactTypes.Clear();
context.Entry(dummy).State = EntityState.Modified;
context.SaveChanges();
}
context.Detach(dummy);
context.Contacts.Attach(contact);
// add currently selected contact types, then save
string[] ids = this.HttpContext.Request.Form["ContactTypes"].Split(',');
for(int i = 0; i< ids.Length; i++)
{
int x = Convert.ToInt32(ids[i]);
ContactType selectedType = context.ContactTypes.Single(t => t.ContactTypeId == x);
contact.ContactTypes.Add(selectedType);
}
context.Entry(contact).State = EntityState.Modified;
context.SaveChanges();
ViewBag.Message = "Save was successful.";
}
ViewData["ContactTypes"] = contact.ContactTypes.Select(t => t.ContactTypeId);
ViewData["ContactTypesAll"] = GetTypesList();
return View(contact);
}
I had to add a Detach method to my DBContext class as well (in CTP 5 this is not exposed):
public void Detach(object entity)
{
((System.Data.Entity.Infrastructure.IObjectContextAdapter)this).ObjectContext.Detach(entity);
}

Resources