Related
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>
I have the following view model
public class ProjectVM
{
....
[Display(Name = "Category")]
[Required(ErrorMessage = "Please select a category")]
public int CategoryID { get; set; }
public IEnumerable<SelectListItem> CategoryList { get; set; }
....
}
and the following controller method to create a new Project and assign a Category
public ActionResult Create()
{
ProjectVM model = new ProjectVM
{
CategoryList = new SelectList(db.Categories, "ID", "Name")
}
return View(model);
}
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Save and redirect
}
and in the view
#model ProjectVM
....
#using (Html.BeginForm())
{
....
#Html.LabelFor(m => m.CategoryID)
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList, "-Please select-")
#Html.ValidationMessageFor(m => m.CategoryID)
....
<input type="submit" value="Create" />
}
The view displays correctly but when submitting the form, I get the following error message
InvalidOperationException: The ViewData item that has the key 'CategoryID' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.
The same error occurs using the #Html.DropDownList() method, and if I pass the SelectList using a ViewBag or ViewData.
The error means that the value of CategoryList is null (and as a result the DropDownListFor() method expects that the first parameter is of type IEnumerable<SelectListItem>).
You are not generating an input for each property of each SelectListItem in CategoryList (and nor should you) so no values for the SelectList are posted to the controller method, and therefore the value of model.CategoryList in the POST method is null. If you return the view, you must first reassign the value of CategoryList, just as you did in the GET method.
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
model.CategoryList = new SelectList(db.Categories, "ID", "Name"); // add this
return View(model);
}
// Save and redirect
}
To explain the inner workings (the source code can be seen here)
Each overload of DropDownList() and DropDownListFor() eventually calls the following method
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata,
string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple,
IDictionary<string, object> htmlAttributes)
which checks if the selectList (the second parameter of #Html.DropDownListFor()) is null
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
which in turn calls
private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
which evaluates the the first parameter of #Html.DropDownListFor() (in this case CategoryID)
....
o = htmlHelper.ViewData.Eval(name);
....
IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
MvcResources.HtmlHelper_WrongSelectDataType,
name, o.GetType().FullName, "IEnumerable<SelectListItem>"));
}
Because property CategoryID is typeof int, it cannot be cast to IEnumerable<SelectListItem> and the exception is thrown (which is defined in the MvcResources.resx file as)
<data name="HtmlHelper_WrongSelectDataType" xml:space="preserve">
<value>The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.</value>
</data>
according to stephens (user3559349) answer, this can be useful:
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList ?? new List<SelectListItem>(), "-Please select-")
or in ProjectVM:
public class ProjectVM
{
public ProjectVM()
{
CategoryList = new List<SelectListItem>();
}
...
}
Most Likely Caused some sort of error redirecting to your page and you not initializing your model's drop down lists again.
Make sure that you initialize your drop downs in either the model's constructor or every time before you send said model to the page.
Otherwise you will need to maintain the state of the drop down lists either through the view bag or through the hidden value helpers.
OK, the poster's canned answer neatly explained why the error occurred, but not how to get it to work. I'm not sure that's really an answer, but it did point me in the right direction.
I ran into the same issue and found a slick way to resolve it. I'll try to capture that here. Disclaimer - I work on web pages once a year or so and really don't know what I'm doing most of the time. This answer should in no way be considered an "expert" answer, but it does the job with little work...
Given that I have some data object (most likely a Data Transfer Object) that I want to use a drop-down list to supply valid values for a field, like so:
public class MyDataObject
{
public int id;
public string StrValue;
}
Then the ViewModel looks like this:
public class MyDataObjectVM
{
public int id;
public string StrValue;
public List<SectListItem> strValues;
}
The real problem here, as #Stephen so eloquently described above, is the select list isn't populated on the POST method in the controller. So your controller methods would look like this:
// GET
public ActionResult Create()
{
var dataObjectVM = GetNewMyDataObjectVM();
return View(dataObjectVM); // I use T4MVC, don't you?
}
private MyDataObjectVM GetNewMyDataObjectVM(MyDataObjectVM model = null)
{
return new MyDataObjectVM
{
int id = model?.Id ?? 0,
string StrValue = model?.StrValue ?? "",
var strValues = new List<SelectListItem>
{
new SelectListItem {Text = "Select", Value = ""},
new SelectListITem {Text = "Item1", Value = "Item1"},
new SelectListItem {Text = "Item2", Value = "Item2"}
};
};
}
// POST
public ActionResult Create(FormCollection formValues)
{
var dataObject = new MyDataObject();
try
{
UpdateModel(dataObject, formValues);
AddObjectToObjectStore(dataObject);
return RedirectToAction(Actions.Index);
}
catch (Exception ex)
{
// fill in the drop-down list for the view model
var dataObjectVM = GetNewMyDataObjectVM();
ModelState.AddModelError("", ex.Message);
return View(dataObjectVM);
)
}
There you have it. This is NOT working code, I copy/pasted and edited to make it simple, but you get the idea. If the data members in both the original data model and the derived view model have the same name, UpdateModel() does an awesome job of filling in just the right data for you from the FormCollection values.
I'm posting this here so I can find the answer when I inevitably run into this issue again -- hopefully it will help someone else out as well.
I had the same problem, I was getting an invalid ModelState when I tried to post the form. For me, this was caused by setting CategoryId to int, when I changed it to string the ModelState was valid and the Create method worked as expected.
In my case the first ID in my list was zero, once I changed the ID to start from 1, it worked.
I have the following view model
public class ProjectVM
{
....
[Display(Name = "Category")]
[Required(ErrorMessage = "Please select a category")]
public int CategoryID { get; set; }
public IEnumerable<SelectListItem> CategoryList { get; set; }
....
}
and the following controller method to create a new Project and assign a Category
public ActionResult Create()
{
ProjectVM model = new ProjectVM
{
CategoryList = new SelectList(db.Categories, "ID", "Name")
}
return View(model);
}
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Save and redirect
}
and in the view
#model ProjectVM
....
#using (Html.BeginForm())
{
....
#Html.LabelFor(m => m.CategoryID)
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList, "-Please select-")
#Html.ValidationMessageFor(m => m.CategoryID)
....
<input type="submit" value="Create" />
}
The view displays correctly but when submitting the form, I get the following error message
InvalidOperationException: The ViewData item that has the key 'CategoryID' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.
The same error occurs using the #Html.DropDownList() method, and if I pass the SelectList using a ViewBag or ViewData.
The error means that the value of CategoryList is null (and as a result the DropDownListFor() method expects that the first parameter is of type IEnumerable<SelectListItem>).
You are not generating an input for each property of each SelectListItem in CategoryList (and nor should you) so no values for the SelectList are posted to the controller method, and therefore the value of model.CategoryList in the POST method is null. If you return the view, you must first reassign the value of CategoryList, just as you did in the GET method.
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
model.CategoryList = new SelectList(db.Categories, "ID", "Name"); // add this
return View(model);
}
// Save and redirect
}
To explain the inner workings (the source code can be seen here)
Each overload of DropDownList() and DropDownListFor() eventually calls the following method
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata,
string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple,
IDictionary<string, object> htmlAttributes)
which checks if the selectList (the second parameter of #Html.DropDownListFor()) is null
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
which in turn calls
private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
which evaluates the the first parameter of #Html.DropDownListFor() (in this case CategoryID)
....
o = htmlHelper.ViewData.Eval(name);
....
IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
MvcResources.HtmlHelper_WrongSelectDataType,
name, o.GetType().FullName, "IEnumerable<SelectListItem>"));
}
Because property CategoryID is typeof int, it cannot be cast to IEnumerable<SelectListItem> and the exception is thrown (which is defined in the MvcResources.resx file as)
<data name="HtmlHelper_WrongSelectDataType" xml:space="preserve">
<value>The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.</value>
</data>
according to stephens (user3559349) answer, this can be useful:
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList ?? new List<SelectListItem>(), "-Please select-")
or in ProjectVM:
public class ProjectVM
{
public ProjectVM()
{
CategoryList = new List<SelectListItem>();
}
...
}
Most Likely Caused some sort of error redirecting to your page and you not initializing your model's drop down lists again.
Make sure that you initialize your drop downs in either the model's constructor or every time before you send said model to the page.
Otherwise you will need to maintain the state of the drop down lists either through the view bag or through the hidden value helpers.
OK, the poster's canned answer neatly explained why the error occurred, but not how to get it to work. I'm not sure that's really an answer, but it did point me in the right direction.
I ran into the same issue and found a slick way to resolve it. I'll try to capture that here. Disclaimer - I work on web pages once a year or so and really don't know what I'm doing most of the time. This answer should in no way be considered an "expert" answer, but it does the job with little work...
Given that I have some data object (most likely a Data Transfer Object) that I want to use a drop-down list to supply valid values for a field, like so:
public class MyDataObject
{
public int id;
public string StrValue;
}
Then the ViewModel looks like this:
public class MyDataObjectVM
{
public int id;
public string StrValue;
public List<SectListItem> strValues;
}
The real problem here, as #Stephen so eloquently described above, is the select list isn't populated on the POST method in the controller. So your controller methods would look like this:
// GET
public ActionResult Create()
{
var dataObjectVM = GetNewMyDataObjectVM();
return View(dataObjectVM); // I use T4MVC, don't you?
}
private MyDataObjectVM GetNewMyDataObjectVM(MyDataObjectVM model = null)
{
return new MyDataObjectVM
{
int id = model?.Id ?? 0,
string StrValue = model?.StrValue ?? "",
var strValues = new List<SelectListItem>
{
new SelectListItem {Text = "Select", Value = ""},
new SelectListITem {Text = "Item1", Value = "Item1"},
new SelectListItem {Text = "Item2", Value = "Item2"}
};
};
}
// POST
public ActionResult Create(FormCollection formValues)
{
var dataObject = new MyDataObject();
try
{
UpdateModel(dataObject, formValues);
AddObjectToObjectStore(dataObject);
return RedirectToAction(Actions.Index);
}
catch (Exception ex)
{
// fill in the drop-down list for the view model
var dataObjectVM = GetNewMyDataObjectVM();
ModelState.AddModelError("", ex.Message);
return View(dataObjectVM);
)
}
There you have it. This is NOT working code, I copy/pasted and edited to make it simple, but you get the idea. If the data members in both the original data model and the derived view model have the same name, UpdateModel() does an awesome job of filling in just the right data for you from the FormCollection values.
I'm posting this here so I can find the answer when I inevitably run into this issue again -- hopefully it will help someone else out as well.
I had the same problem, I was getting an invalid ModelState when I tried to post the form. For me, this was caused by setting CategoryId to int, when I changed it to string the ModelState was valid and the Create method worked as expected.
In my case the first ID in my list was zero, once I changed the ID to start from 1, it worked.
I've got his code.
PartialView.
<div class="input width110">
#Html.EditorFor(x => x.Price, #Html.Attributes(#class: "right_text_align", #disabled: "true", #id: "Price"))
</div>
Model.
public class ServiceModel
{
[DisplayFormat(DataFormatString = "{0:0.00}", ApplyFormatInEditMode = true)]
public decimal Price { get; set; }
}
Controller
public ActionResult SetService(ServiceModel model, string action)
{
if (ModelState.IsValid)
{
/*Does smthg.*/
ModelState.Clear();
}
return View("Index", rcpModel);
//Index is main view, which holds partialView
//rcpModel holds, model
}
When view loads Decimal is displayed in format "0.00". But after post when modelState is invalid number in displayed in format "0.0000". If model state isvalid, everything goes well. Has anyone encountered anything similar?
If you have javascript modifying the values on textboxes (currency formatting or commas) then you might be getting binding errors because it will behave as a string. Try this:
Create a BindingProperty for decimal values
public class DecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
actualValue = Convert.ToDecimal(valueResult.AttemptedValue,
CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
On your global.asax app_start or WebActivator.PostApplicationStartMethod add an entry to register the custom binder:
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
To display dot instead of comma is enough to change the culture to english in every point of the code which is used before the view is called.
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("En");
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