6

I'm trying to work out the best architecture for handling model type hierarchies within an MVC application.

Given the following hypothetical model -

public abstract class Person
{
    public string Name { get; set; }
}

public class Teacher : Person
{
    public string Department { get; set; }
}

public class Student : Person
{
    public int Year { get; set; }
}

I could just create a controller for each type. Person would have just the index and detail action with the views making use of display templates, Teacher and Student would have just the Create/Edit actions. That would work but seems wasteful and wouldn't really scale because a new controller and views would be needed if another type was added to the hierarchy.

Is there a way to make a more generic Create/Edit action within the Person controller? I have searched for the answer for a while but can't seem to find exactly what I am looking for so any help or pointers would be appreciated :)

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
mcricker
  • 61
  • 1
  • 2
  • 2
    Actually Controller in MVC is only a bridge between View and Model, the logic itself should be in the Model layer and should be called by controller(s). You do not need controller for each class. – libik Aug 07 '14 at 15:56
  • 2
    and how would that work? – mcricker Aug 07 '14 at 16:02

1 Answers1

11

Sure, but it takes a little leg work.

First, in each of your edit/create views, you need to emit the type of model you are editing.

Second, you need add a new modelbinder for the person class. Here is a sample of why I would do for that:

public class PersonModelBinder :DefaultModelBinder
{

    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        PersonType personType = GetValue<PersonType>(bindingContext, "PersonType");

        Type model = Person.SelectFor(personType);

        Person instance = (Person)base.CreateModel(controllerContext, bindingContext, model);

        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, model);

        return instance;
    }

    private T GetValue<T>(ModelBindingContext bindingContext, string key)
    {
        ValueProviderResult valueResult =bindingContext.ValueProvider.GetValue(key);

        bindingContext.ModelState.SetModelValue(key, valueResult);

        return (T)valueResult.ConvertTo(typeof(T));
    }  
}

Register it in your app start:

ModelBinders.Binders.Add(typeof(Person), new PersonModelBinder());

The PersonType is what I tend to use in each model and is an enum that says what each type is, I emit that in a HiddenFor so that it comes back in with the post data.

The SelectFor is a method that returns a type for the specified enum

public static Type SelectFor(PersonType type)
    {
        switch (type)
        {
            case PersonType.Student:
                return typeof(Student);
            case PersonType.Teacher:
                return typeof(Teacher);
            default:
                throw new Exception();
        }
    }

You can now do something like this in your controller

public ActionResult Save(Person model)
{
    // you have a teacher or student in here, save approriately
}

Ef is able to deal with this quite effectively with TPT style inheritance

Just to complete the example:

public enum PersonType
{
    Teacher,
    Student    
}

public class Person
{ 
    public PersonType PersonType {get;set;}
}

public class Teacher : Person
{
    public Teacher()
    {
        PersonType = PersonType.Teacher;
    }
}
Slicksim
  • 7,054
  • 28
  • 32
  • Question, what is `PersonType`? I don't see it defined anywhere? – T McKeown Aug 07 '14 at 16:15
  • 1
    Its just an enum, you could emit the type to use directly, but i prefer to obfuscate that and use an enum for safety – Slicksim Aug 07 '14 at 16:21
  • Thanks for the detailed reply :) I think i'm missing something though. The model binder is called when the Create action is called with a Person so this is after the Create form has been filled in and submitted back to the controller. How would I be able to select the which view (or partial view) was displayed to the user so that they could fill in the correct form in the first place? – mcricker Aug 07 '14 at 16:41
  • 1
    If you use EditorFor and then name the view in the EditorTemplates in the controllers View folder or Shared folder as the same name as the class, MVC will find it via convention :) – Slicksim Aug 07 '14 at 17:10
  • Do you mean use @Html.Editor in the Person Create view? In that way, if I supplied the name of the derived class, and had an editor template of the same name, I assume that the call to @Html.Editor(derivedClassName) would insert the template? – mcricker Aug 07 '14 at 17:36
  • I meant @Html.EditorForModel(derivedClassName) – mcricker Aug 07 '14 at 17:49
  • You can do @Html.EditorForModel(). Mvc will then scour the EditorTemplates folders for the class name of the model. So you dont need to provide it, it will look for it by that name. means in your create or edit, you can use a stub view called Create or Edit and just call editorformodel and mvc will insert the right view for you. – Slicksim Aug 08 '14 at 07:53
  • You'll want to have `using System.Web.Mvc;` – snumpy Oct 05 '17 at 15:50
  • Nice solution, this worked well for me. I don't see the need for the line in GetValue about setting the model value though. I still get the model value in my post method even without that line. What is that line supposed to be doing? – Frank Hoffman Mar 21 '18 at 20:33