问题
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