Entity Framework POCO to ViewModel in MVC 3

自作多情 提交于 2020-01-05 16:38:07

问题


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?


回答1:


IMO Solution 2 is the right way forward, as you will often find that the EF POCOs and ViewModels need to diverge as they address different concerns.

e.g. a likely concern is decorating your ViewModels with presentation tier annotations (UIHints, ValidationAttributes etc.)

Solution 1 as you say will lead to bloat and you will probably wind up referencing System.Data.Annotations (probably OK) but you could also reference System.Data.MVC if you need [HiddenInput] etc

IMO Solution 3 winds up being more effort than a new ViewModel - e.g. although MetadataType allows you to 'shift' the attributes onto another class with similar properties, this is an awful amount of effort.

e.g. with Solution 3 you would probably wind up with

namespace EFPocos
{
    /// <summary>
    ///  Your EF POCO
    /// </summary>
    public class Question
    {
        public virtual int Id { get; set; }
        public virtual string QuestionText { get; set; }
        public virtual int QuestionGroupId { get; set; }
    }
}

namespace UIViewModels
{
    /// <summary>
    ///  Your ViewModel 'derivative', but sans Annotation decoration
    /// </summary>
    [MetadataType(typeof(QuestionUIMetaData))]
    public class QuestionViewModel : EFPocos.Question, IValidatableObject
    {
        public string SubmittedValue { get; set; }

        #region IValidatableObject Members

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Id % 2 == 0)
            {
                yield return new ValidationResult("Some rule has fired");
            }
        }

        #endregion
    }

    /// <summary>
    /// Annotations go here ... and we may as well just AutoMapped a simple ViewModel
    /// </summary>
    public class QuestionUIMetaData
    {
        [HiddenInput]
        public int Id { get; set; }
        [Required()]
        public string QuestionText { get; set; }
        [Required()]
        [DisplayName("Select Group ...")]
        public int QuestionGroupId { get; set; }
        [DisplayName("Question is Here")]
        [StringLength(50, ErrorMessage = "Too Long!!")]
        public string SubmittedValue { get; set; }
    }
}



回答2:


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.



来源:https://stackoverflow.com/questions/12315971/entity-framework-poco-to-viewmodel-in-mvc-3

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!