Search code examples
c#asp.net-mvc-3viewmodelwcf-data-services

Entity Framework POCO to ViewModel in MVC 3


I have an example project, a dynamic questionnaire system where any administrator can create a questionnaire, then add groups of questions to it and in-turn add questions to each question group.

Take the following group of POCOs that make up the entities for my EF data context:

public class Questionnaire
{
    public virtual int Id { get; set; }
    public virtual string QuestionnaireName { get; set; }
    public virtual IList<QuestionGroup> QuestionGroups { get; set; }
}

public class QuestionGroup
{
    public virtual int Id { get; set; }
    public virtual string GroupName { get; set; }
    public virtual int QuestionnaireId { get; set; }
    public virtual IList<Question> Questions { get; set; }
}

public class Question
{
    public virtual int Id { get; set; }
    public virtual string QuestionText { get; set; }
    public virtual int QuestionGroupId { get; set; }
    public virtual QuestionGroup QuestionGroup { get; set; }
}

I am accessing these entities in my Web UI via WCF Data Services and am wondering what a best practice (or at least a cleaner way) of handling input in my view for these entities. The following are some of the ideas I have of overcoming this, but I'm having a hard time liking any of them because they just feel a but convoluted.

Solution 1

Add a property to my Question entity called SubmittedValue and have my EF data context Ignore(m => m.SubmittedValue) this. This property is what I will use to persist the input value for the Question at the view level.

What I don't like about this, is bloating my POCO entity with pretty much irrelevant properties - I'm only going to use SubmittedValue in one case at the Web UI whereas my POCO entities will be used elsewhere many times.

Solution 2

Create view model objects that have the same structure as my POCOs, let's call them QuestionnaireModel, QuestionGroupModel and QuestionModel - these are initialised in my controller and properties are copied from the POCO to the view model. On QuestionModel I add my SubmittedValue property and persist this value using a custom model binder which looks at the binding context and gets my values from the view - where the name looks something like [group.question.1] (where 1 is the Id of the question). This is presented in the view using Editor Templates for each question group and for each question.

What I don't like about this, is bloating my web UI with these extra view model objects and having to manually copy property values from my POCO to the view model. I'm aware I could use something like AutoMapper to do this for me, but this is just automating that work, where I'd ideally like to not do it at all.

Solution 3

Change solution 2 slighly, to instead extend my POCOs and override the virtual collection properties with the other view model objects. So, my view model would look like this:

public class QuestionnaireModel : Questionnaire
{
    public new IList<QuestionGroupModel> QuestionGroups { get; set; }
}

public class QuestionGroupModel : QuestionGroup
{
    public new IList<Question> Questions { get; set; }
}

public class QuestionModel : Question
{
    public string SubmittedValue { get; set; }
}

I do like this idea the best, but I haven't actually tried this yet. I get the best of both worlds here as 1. I can keep my POCOs out of my views and 2. I keep that one-time use property SubmittedValue out of my business layer.

Do any of you have a better way of handling this?


Solution

  • Having played around with solution 3 (which was my preferred solution) I've managed to finally get it. Here's what I'm doing, for anyone who stumbles on this question. First, I create my view models that extend my POCO entities. I override the collection properties with a new implementation to make my collections my view model types. I then add my form persistence property to my Question view model (so I can keep it out of my business layer).

    public class QuestionnaireModel : Questionnaire
    {
        public new IList<QuestionGroupModel> QuestionGroups { get; set; }
    }
    
    public class QuestionGroupModel : QuestionGroup
    {
        public new IList<QuestionModel> Questions { get; set; }
    }
    
    public class QuestionModel : Question
    {
        public string SubmittedValue { get; set; }
    }
    

    Using AutoMapper I create the mappings between my POCOs and view models like so (using .AfterMap() to make sure my persistence property isn't a null but an empty string instead):

    Mapper.CreateMap<Questionnaire, QuestionnaireModel>();
    Mapper.CreateMap<QuestionGroup, QuestionGroupModel>();
    Mapper.CreateMap<Question, QuestionModel>().AfterMap((s, d) => d.SubmittedValue = "");
    

    Next, each Question has an editor template that has a single input element:

    @Html.Raw(string.Format("<input type=\"text\" name=\"group.question.{0}\" value=\"{1}\" />", Model.Id.ToString(), Model.SubmittedValue)
    

    Finally, I pick up (and persist) these values using a custom model binder, like so:

    int id = Int32.Parse(controllerContext.RouteData.Values["id"].ToString());
    
    var questionnaire = _proxy.Questionnaires
        .Expand("QuestionGroups")
        .Expand("QuestionGroups/Questions")
        .Where(q => q.Id == id)
        .FirstOrDefault();
    
    var model = Mapper.Map<Questionnaire, QuestionnaireModel>(questionnaire);
    
    foreach (var group in model.QuestionGroups)
    {
        foreach (var question in group.Questions)
        {
            string inputValueId = "group.question." + question.Id.ToString();
            string value = bindingContext.ValueProvider.GetValue(inputValueId).AttemptedValue;
    
            question.SubmittedValue = value;
        }
    }
    

    Although I'm not too happy about the custom model binder (I don't think I'm setting my editor templates up correctly so resorting to a custom binder) for me this is the preferred solution.