Question

I have a lot of similar ViewModel:

public class RequestForSalaryVM : StatementViewModel
{
  // RequestForSalaryVM properties
}

public class ReliefVM : StatementViewModel
{
  // ReliefVM properties
}

and a lot of similar methods:

[HttpPost]
public ActionResult SaveRelief(User currentUser, ReliefVM statement)
{
    ReliefVM model = (ReliefVM)SaveModel(currentUser, statement);
    if (model == null)
        return RedirectToAction("List");
    return View("Relief", model);
}

[HttpPost]
public ActionResult SaveRequestForSalary(User currentUser, RequestForSalaryVM statement)
{
    RequestForSalaryVM model = (RequestForSalaryVM)SaveModel(currentUser, statement);
    if (model == null)
        return RedirectToAction("List");
    return View("RequestForSalary", model);
}

I want to get something like this:

[HttpPost]
public ActionResult SaveStatement(User currentUser, FormCollection statement, string ViewModelName)
{
  Assembly assembly = typeof(SomeKnownType).Assembly;
  Type type = assembly.GetType(ViewModelName);
  object ViewModel = Activator.CreateInstance(type);

  //Fill ViewModel from FormCollection  <= how can I use asp.net mvc binding for this?
  //I do not want to create their own implementation of asp.net mvc binding 
    return View(ViewModelName, ViewModel);
}
Was it helpful?

Solution

You can try Controller.UpdateModel or Controller.TryUpdateModel method:

[HttpPost]
public ActionResult SaveStatement(User currentUser, FormCollection statement, string ViewModelName)
{
    ...
    object ViewModel = Activator.CreateInstance(type);
    if (TryUpdateModel(viewModel))
    {
        // save the ViewModel
    }   

    return View(ViewModelName, ViewModel);
}

However I would suggest you to create a custom ModelBinder, as it is its responsibility to create and populate model properties.

I can show you a simple example how you can achieve this:

Base ViewModel

public abstract class StatementViewModel
{
    public abstract StatementType StatementType { get; }
    ...
}

public enum StatementType
{
    Relief,
    RequestForSalary,
    ...
}

ViewModels

public class RequestForSalaryVM : StatementViewModel
{
    public override StatementType StatementType
    {
        get { return StatementType.RequestForSalary; }
    }
    ...
}

public class ReliefVM : StatementViewModel
{
    public override StatementType StatementType
    {
        get { return StatementType.Relief; }
    }
    ...
}

ModelBinder

public class StatementModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var statementTypeParameter = bindingContext.ValueProvider.GetValue("StatementType");
        if (statementTypeParameter == null)
            throw new InvalidOperationException("StatementType is not specified");

        StatementType statementType;
        if (!Enum.TryParse(statementTypeParameter.AttemptedValue, true, out statementType))
            throw new InvalidOperationException("Incorrect StatementType"); // not sure about the type of exception

        var model = SomeFactoryHelper.GetStatementByType(statementType); // returns an actual model by StatementType parameter
                                                                         // this could be a simple switch statement
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, model.GetType());
        bindingContext.ModelMetadata.Model = model;
        return model;
    }
}

After that register the model binder in the Global.asax:

ModelBinders.Binders.Add(typeof(StatementViewModel), new StatementModelBinder());

Controller

[HttpPost]
public ActionResult Index(StatementViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        // save the model
    }
    return View(viewModel);
}

OTHER TIPS

You can probably solve the problem with a CustomModelBinder like this:

public class StatementVMBinder : DefaultModelBinder
{
    // this is the only method you need to override:
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        if (modelType == typeof(StatementViewModel)) // so it will leave the other VM to the default implementation.
        {
            // this gets the value from the form collection, if it was in an input named "ViewModelName":
            var discriminator = bindingContext.ValueProvider.GetValue("ViewModelName");
            Type instantiationType;
            if (discriminator == "SomethingSomething")
                instantiationType = typeof(ReliefVM);
            else // or do a switch case
                instantiationType = typeof(RequestForSalaryVM);

            var obj = Activator.CreateInstance(instantiationType);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, instantiationType);
            bindingContext.ModelMetadata.Model = obj;
            return obj;
        }
        return base.CreateModel(controllerContext, bindingContext, modelType);
    }
}

Your action will need this signature:

public ActionResult SaveStatement(User currentUser, StatementViewModel viewModel)

but the viewModel you receive in the method will be of the appropriate derived type, so you should be able to cast it as you are doing in the individual methods.

The only thing left is to register the Custom Binder in the Global.asax.

Did you try to use UpdateModel or TryUpdateModel to initialize your model values from form collection? Look at the code example below

[HttpPost]
public ActionResult SaveStatement(User currentUser, FormCollection statement, string    ViewModelName)
{
  Assembly assembly = typeof(SomeKnownType).Assembly;
  Type type = assembly.GetType(ViewModelName);
  object ViewModel = Activator.CreateInstance(type);

  if (!TryUpdateModel(ViewModel, statement.ToValueProvider()))
  {
     //some another actions
  }

  return View(ViewModelName, ViewModel);
}

If I were you, I'd use a DTO (Data Transfer Object) wrapping the name of the view and a ViewModel accessed via an Interface. You then have something like:

[HttpPost]
public ActionResult SaveStatement(User currentUser, VMWrapper wrapper)
{
    IVM model = SaveModel(currentUser, wrapper.Statement);
    if (model == null)
        return RedirectToAction("List");
    return View(wrapper.ViewName, model);
}

But this supposes that your views can handle the differences between the VMs...

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top