0

I'm developing a registration flow where user comes and fills 5 pages to complete a process. I decided to have multiple views and one controller and a ProcessNext action method to go step by step. Each time Process Next gets called it gets the origin view and next view. Since each view associated with there own view model i have created a base view model which all view specific view model derived from. Now the issue is, casting is throwing an exception.. here is the sample code

Base View Model

public class BaseViewModel
{
     public string viewName;
}

Personal View Model

public class PersonalViewModel : BaseViewModel
{
    public string FirstName;
    // rest properties comes here
}

Index.cshtml

@Model PersonalViewModel

   @using (Html.BeginForm("ProcessNext", "Wizard", FormMethod.Post, new { class = "form-horizontal", role = "form" }))
@Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" })
<input type="submit" class="btn btn-default" value="Register" />

Basically, I'm binding the view with PersonalViewModel here

Now in Controller ProcessNext Action method looks like this.

public ActionResult ProcessNext(BaseViewModel viewModelData)
{
  PersonalViewModel per = (PersonalViewModel) viewModelData;
}

This is failing and throwing a type case exception, why?..

My idea is to use only one action method to transform all these derived view model and send to a common class to validate and process. Please help me to get through this issue.. Thanks!

user2066540
  • 357
  • 2
  • 5
  • 16
  • Because `viewModelData` is not `PersonalViewModel` You can't cast a base type to a derived type. And in any case, this cant possibly work because your posting back `BaseViewModel` so everything related to `PersonalViewModel` is lost. –  Nov 15 '15 at 08:43
  • In basic oops.. i'm good to case a base class type to derived .. right?.. So what can be done here to get this working? any wild guess? – user2066540 Nov 15 '15 at 08:58
  • In the first step, your post back to `public ActionResult Step1(PersonalViewModel model)` which saves the data then redirects to a GET method that renders your `Step2ViewModel` view which posts back to `public ActionResult Step2(Step2ViewModel model)` etc. There is no point in a `BaseViewModel` except to perhaps include an `ID` property that will be common to all step models –  Nov 15 '15 at 09:02
  • And _"i'm good to cast a base class type to derived .. right?"_ No, you cannot do that. But you can do it the other way around. –  Nov 15 '15 at 09:07

2 Answers2

0

The reason that you see this exception is that your model type is BaseViewModel and not PersonalViewModel. Model binder is the one that creates a model and since your action's model is BaseViewModel it creates a BaseViewModel object.

I would recommend you to create separate actions for each one of your steps. Each action should have its corresponding model. I also think that you should prefer with composition instead of inheritance in this case.

public class FullModel
{
    public FirstStepModel FirstStep {get;set;}
    public SecondStepModel SecondStep {get;set;}
}

Then once you start your flow (on a first step for example) you can create a FullModel object and store it somewhere (session/cookie/serialize into a text and send to client - it is really up to you).

Then in controller you will have

[HttpGet]
public ActionResult ProcessFirst()
{       
    HttpContext.Session["FullModel"] = new FullModel(); //at the beginning  store full model in session 
    var firstStepModel = new FirstsStepModel();
    return View(firstStepModel) //return a view for first step
}


[HttpPost]
public ActionResult ProcessFirst(FirstStepModel model)
{
    if(this.ModelState.IsValid)
    {
        var fullModel = HttpContext.Session["FullModel"] as FullModel; //assuming that you stored it in session variable with name "FullModel"
        if(fullModel == null)
        {
           //something went wrong and your full model is not in session..
           //return some error page
        }
        fullModel.FirstStep = model;
        HttpContext.Session["FullModel"] = fullModel; // update your session with latest model
        var secondStepModel = new SecondStepModel();
        return View("SecondStepView", secondStepModel) //return a view for second step
    }

    // model is invalid ...
    return View("FirstStepView", model);    
}

[HttpPost]
public ActionResult ProcessSecond(SecondStepModel model)
{
    var fullModel = HttpContext.Session["FullModel"] as FullModel; //assuming that you stored it in session variable with name "FullModel"
    if(fullModel == null)
    {
        //something went wrong and your full model is not in session..
        //return some error page
    }
    fullModel.SecondStep = model;
    HttpContext.Session["FullModel"] = fullModel; // update your session with latest model
    var thirdStepModel = new ThirdStepModel();
    return View("ThirdStepModel", thirdStepModel); //return a view for a third step
}

Of course you should extract all the shared code to some reusable method. And it is entirely up to you what persistence technique to use for passing FullModel between the request.

If you still prefer to go with one Action solution you need to create a custom model binder that is going create derived instances based on some data that is passed from the client. Take a look at this thread

Community
  • 1
  • 1
Alex Art.
  • 8,711
  • 3
  • 29
  • 47
0

I figured it out a generic way to handle this situation using Model Binders. Here it is.. You might need to have a extended model binder from DefaultBinder to implement to return your model type.

public class WizardModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var viewIdContext = bindingContext.ValueProvider.GetValue("ViewId");
        int StepId = 0;

        if (!int.TryParse(viewIdContext, out StepId))
            throw new InvalidOperationException("Incorrect view identity");
        //This is my factory who gave me child view based on the next view id you can play around with this logic to identify which view should be rendered next
        var model = WizardFactory.GetViewModel(StepId);

        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, model.GetType());
        bindingContext.ModelMetadata.Model = model;
        return model;
    }
}

You would register this binder from your gloab asx like

 ModelBinders.Binders.Add(typeof(BaseViewModel), new WizardModelBinder());

Thanks to all who responsed to my query..!! Let me know if you guys have any questions.

Happy coding..!!

user2066540
  • 357
  • 2
  • 5
  • 16