【转】【英文】ASP.NET MVC View Model Patterns
2010-09-04 09:07
429 查看
SinceMVChasbeenreleasedIhaveobservedmuchconfusionabouthowbesttoconstructviewmodels.Sometimesthisconfusionisnotwithoutgoodreasonsincetheredoesnotseemtobeatonofinformationoutthereonbestpracticerecommendations.Additionally,thereisnota“onesizefitsall”solutionthatactsasthesilverbullet.Inthispost,I’lldescribeafewofthemainpatternsthathaveemergedandthepros/consofeach.Itisimportanttonotethatmanyofthesepatternshaveemergedfrompeoplesolvingreal-worldissues.
Anotherkeypointtorecognizeisthatthequestionofhowbesttoconstructviewmodelsis*not*uniquetotheMVCframework.ThefactisthatevenintraditionalASP.NETwebformsyouhavethesameissues.Thedifferenceisthathistoricallydevelopershaven’talwaysdealtwithitdirectlyinwebforms–insteadwhatoftenhappensisthatthecode-behindfilesendupasmonolithicdumpinggroundsforcodethathasnoseparationofconcernswhatsoeverandiswiringupviewmodels,performingpresentationlogic,performingbusinesslogic,dataaccesscode,andwhoknowswhatelse.MVCatleastfacilitatesthedevelopertakingacloserlookathowtomoreelegantlyimplementSeparationofConcerns.
Pattern1–Domainmodelobjectuseddirectlyastheviewmodel
Consideradomainmodelthatlookslikethis:
[code]{
[/code]
Whenwepassthisintotheview,itofcourseallowsustowritesimpleHTMLhelpersinthestyleofourchoosing:
[code]<%=Html.TextBoxFor(m=>m.Make)%>
[/code]
Andofcoursewithdefaultmodelbindingweareabletopassthatbacktothecontrollerwhentheformisposted:
Whilethisfirstpatternissimpleandcleanandelegant,itbreaksdownfairlyquicklyforanythingbutthemosttrivialviews.Wearebindingdirectlytoourdomainmodelinthisinstance–thisoftenisnotsufficientforfullydisplayingaview.
Pattern2–Dedicatedviewmodelthat*contains*thedomainmodelobject
StayingwiththeMotorcycleexampleabove,amuchmorereal-worldexampleisthatourviewneedsmorethanjustaMotorcycleobjecttodisplayproperly.Forexample,theMakeandModelwillprobablybepopulatedfromdropdownlists.Therefore,acommonpatternistointroduceaviewmodelthatactsasacontainerforallobjectsthatourviewrequiresinordertorenderproperly:
[code]{
[/code]
Inthisinstance,thecontrolleristypicallyresponsibleformakingsureMotorcycleViewModeliscorrectlypopulatedfromtheappropriatedataintherepositories(e.g.,gettingtheMotorcyclefromthedatabase,gettingthecollectionsofMakes/Modelsfromthedatabase).OurHtmlHelperschangeslightlybecausetheyrefertoMotorcycle.MakeratherthanMakedirectly:
Whentheformisposted,wearestillabletohaveastrongly-typedSave()method:
NotethatinthisinstancewehadtousetheBindattributedesignating“Motorcycle”astheprefixtotheHTMLelementswewereinterestedin(i.e.,theonesthatmadeuptheMotorcycleobject).
Thispatternissimpleandelegantandappropriateinmanysituations.However,asviewsbecomemorecomplicated,italsostartstobreakdownsincethereisoftenanimpedancemismatchbetweendomainmodelobjectsandviewmodelobjects.
Pattern3–Dedicatedviewmodelthatcontainsacustomviewmodelentity
Asviewsgetmorecomplicateditisoftendifficulttokeepthedomainmodelobjectinsyncwithconcernsoftheviews.Inkeepingwiththeexampleabove,supposewehadrequirementswhereweneedtopresenttheuseracheckboxattheendofthescreeniftheywanttoaddanothermotorcycle.Whentheformisposted,thecontrollerneedstomakeadeterminationbasedonthisvaluetodeterminewhichviewtoshownext.Thelastthingwewanttodoistoaddthispropertytoourdomainmodelsincethisisstrictlyapresentationconcern.Insteadwecancreateacustom“viewmodelentity”insteadofpassingtheactualMotorcycledomainmodelobjectintotheview.We’llcallitMotorcycleData:
[code]{
[/code]
Thispatternrequiresmoreworkanditalsorequiresa“mapping”translationlayertomapbackandforthbetweentheMotorcycleandMotorcycleDataobjectsbutitisoftenwellworththeeffortasviewsgetmorecomplex.ThispatternisstronglyadvocatedbytheauthorsofMVCinAction(abookahighlyrecommend).TheseideasarefurtherexpandedinapostbyJimmyBogard(oneoftheco-authors)inhispostHowwedoMVC–ViewModels.IstronglyrecommendedreadingBogard’spost(therearemanyinterestingcommentsonthatpostaswell).InithediscussesapproachestohandlingthispatternincludingusingMVCActionfiltersandAutoMapper(IalsorecommendcheckingoutAutoMapper).
Let’scontinuetobuildoutthispatternwithouttheuseofActionfiltersasanalternative.Inreal-worldscenarios,theseviewmodelscangetcomplexfast.NotonlydoweneedtomapthedatafromMotorcycletoMotorcycleData,butwealsomighthavenumerouscollectionsthatneedtobepopulatedfordropdownlists,etc.Ifweputallofthiscodeinthecontroller,thenthecontrollerwillquicklyendupwithalotofcodededicatedjusttobuildingtheviewmodelwhichisnotdesirableaswewanttokeepourcontrollersthin.Therefore,wecanintroducea“builder”classthatisconcernedwithbuildingtheviewmodel.
[code]{
[/code]
Thisallowsourcontrollercodetolooksomethinglikethis:
[code]{
[/code]
Ourviewscanlookprettymuchthesameaspattern#2butnowwehavethecomfortofknowingthatwe’reonlypassinginthedatatotheviewthatweneed–nomore,noless.Whentheformispostedback,ourcontroller’sSave()methodcannowlooksomethinglikethis:
[code]{
[/code]
Conceptually,thisimplementationisverysimilartoBogard’spostbutwithouttheAutoMapattribute.TheAutoMapattributeallowsustokeepsomeofthiscodeoutofthecontrollerwhichcanbequitenice.Oneadvantagetonotusingitisthatthecodeinsidethecontrollerclassismoreobviousandexplicit.Additionally,ourbuilderandmapperclassesmightneedtobuildtheobjectsfrommultiplesourcesandrepositories.Internallyinourmapperclasses,youcanstillmakegreatuseoftoolslikeAutoMapper.
Inmanycomplexreal-worldcases,somevariationofpattern#3isthebestchoiceasitaffordsthemostflexibilitytothedeveloper.
Considerations
Howdoyoudeterminethebestapproachtotake?Herearesomeconsiderationstokeepinmind:
CodeRe-use–Certainlypatterns#1and#2lendthemselvesbesttocodere-useasyouarebindingyourviewsdirectlytoyourdomainmodelobjects.Thisleadstoincreasedcodebrevityasmappinglayersarenotrequired.However,ifyourviewconcernsdifferfromyourdomainmodel(whichtheyoftenwill)options#1and#2begintobreakdown.
Impedancemismatch–Oftenthereisanimpedancemismatchbetweenyourdomainmodelandtheconcernsofyourview.Inthesecases,option#3givesthemostflexibility.
MappingLayer–Ifcustomviewentitiesareusedasinoption#3,youmustensureyouestablishapatternforamappinglayer.Althoughthismeansmorecodethatmustbewritten,itgivesthemostflexibilityandtherearelibrariesavailablesuchasAutoMapperthatmakethiseasiertoimplement.
Validation–Althoughtherearemanywaystoperformvalidation,oneofthemostcommonistouselibrarieslikeDataAnnotations.Althoughtypicalvalidations(e.g.,requiredfields,etc.)willprobablybethesamebetweenyourdomainmodelsandyourviews,notallvalidationwillalwaysmatch.Additionally,youmaynotalwaysbeincontrolofyourdomainmodels(e.g.,insomeenterprisesthedomainmodelsareexposedviaservicesthatUIdeveloperssimplyconsume).Sothereisalimittohowyoucanassociatevalidationswiththoseclasses.Yes,youcanuseaseparate“metadata”classtodesignatevalidationsbutthisduplicatessomecodesimilartohowaviewmodelentityfromoption#3wouldanyway.Therefore,option#3givesyoutheabsolutemostcontroloverUIvalidation.
Conclusion
Thefollowinghasbeenasummaryofseveralofthepatternsthathaveemergedindealingwithviewmodels.AlthoughthesehaveallbeeninthecontextofASP.NETMVC,theproblemwithhowbesttodealwithviewmodelsisalsoanissuewithotherframeworkslikewebformsaswell.Ifyouareabletobinddirectlytodomainmodelinsimplecases,thatisthesimplestandeasiestsolution.However,asyourcomplexitygrows,havingdistinctviewmodelsgivesyouthemostoverallflexibility.
Anotherkeypointtorecognizeisthatthequestionofhowbesttoconstructviewmodelsis*not*uniquetotheMVCframework.ThefactisthatevenintraditionalASP.NETwebformsyouhavethesameissues.Thedifferenceisthathistoricallydevelopershaven’talwaysdealtwithitdirectlyinwebforms–insteadwhatoftenhappensisthatthecode-behindfilesendupasmonolithicdumpinggroundsforcodethathasnoseparationofconcernswhatsoeverandiswiringupviewmodels,performingpresentationlogic,performingbusinesslogic,dataaccesscode,andwhoknowswhatelse.MVCatleastfacilitatesthedevelopertakingacloserlookathowtomoreelegantlyimplementSeparationofConcerns.
Pattern1–Domainmodelobjectuseddirectlyastheviewmodel
Consideradomainmodelthatlookslikethis:
publicclassMotorcycle
[code]{
publicstringMake{get;set;}
publicstringModel{get;set;}
publicintYear{get;set;}
publicstringVIN{get;set;}
}
[/code]
Whenwepassthisintotheview,itofcourseallowsustowritesimpleHTMLhelpersinthestyleofourchoosing:
<%=Html.TextBox("Make")%>
[code]<%=Html.TextBoxFor(m=>m.Make)%>
[/code]
Andofcoursewithdefaultmodelbindingweareabletopassthatbacktothecontrollerwhentheformisposted:
publicActionResultSave(Motorcyclemotorcycle)
Whilethisfirstpatternissimpleandcleanandelegant,itbreaksdownfairlyquicklyforanythingbutthemosttrivialviews.Wearebindingdirectlytoourdomainmodelinthisinstance–thisoftenisnotsufficientforfullydisplayingaview.
Pattern2–Dedicatedviewmodelthat*contains*thedomainmodelobject
StayingwiththeMotorcycleexampleabove,amuchmorereal-worldexampleisthatourviewneedsmorethanjustaMotorcycleobjecttodisplayproperly.Forexample,theMakeandModelwillprobablybepopulatedfromdropdownlists.Therefore,acommonpatternistointroduceaviewmodelthatactsasacontainerforallobjectsthatourviewrequiresinordertorenderproperly:
publicclassMotorcycleViewModel
[code]{
publicMotorcycleMotorcycle{get;set;}
publicSelectListMakeList{get;set;}
publicSelectListModelList{get;set;}
}
[/code]
Inthisinstance,thecontrolleristypicallyresponsibleformakingsureMotorcycleViewModeliscorrectlypopulatedfromtheappropriatedataintherepositories(e.g.,gettingtheMotorcyclefromthedatabase,gettingthecollectionsofMakes/Modelsfromthedatabase).OurHtmlHelperschangeslightlybecausetheyrefertoMotorcycle.MakeratherthanMakedirectly:
<%=Html.DropDownListFor(m=>m.Motorcycle.Make,Model.MakeList)%>
Whentheformisposted,wearestillabletohaveastrongly-typedSave()method:
publicActionResultSave([Bind(Prefix="Motorcycle")]Motorcyclemotorcycle)
NotethatinthisinstancewehadtousetheBindattributedesignating“Motorcycle”astheprefixtotheHTMLelementswewereinterestedin(i.e.,theonesthatmadeuptheMotorcycleobject).
Thispatternissimpleandelegantandappropriateinmanysituations.However,asviewsbecomemorecomplicated,italsostartstobreakdownsincethereisoftenanimpedancemismatchbetweendomainmodelobjectsandviewmodelobjects.
Pattern3–Dedicatedviewmodelthatcontainsacustomviewmodelentity
Asviewsgetmorecomplicateditisoftendifficulttokeepthedomainmodelobjectinsyncwithconcernsoftheviews.Inkeepingwiththeexampleabove,supposewehadrequirementswhereweneedtopresenttheuseracheckboxattheendofthescreeniftheywanttoaddanothermotorcycle.Whentheformisposted,thecontrollerneedstomakeadeterminationbasedonthisvaluetodeterminewhichviewtoshownext.Thelastthingwewanttodoistoaddthispropertytoourdomainmodelsincethisisstrictlyapresentationconcern.Insteadwecancreateacustom“viewmodelentity”insteadofpassingtheactualMotorcycledomainmodelobjectintotheview.We’llcallitMotorcycleData:
publicclassMotorcycleData
[code]{
publicstringMake{get;set;}
publicstringModel{get;set;}
publicintYear{get;set;}
publicstringVIN{get;set;}
publicboolAddAdditionalCycle{get;set;}
}
[/code]
Thispatternrequiresmoreworkanditalsorequiresa“mapping”translationlayertomapbackandforthbetweentheMotorcycleandMotorcycleDataobjectsbutitisoftenwellworththeeffortasviewsgetmorecomplex.Thispatternisstronglyadvocatedbytheauthorsof
Let’scontinuetobuildoutthispatternwithouttheuseofActionfiltersasanalternative.Inreal-worldscenarios,theseviewmodelscangetcomplexfast.NotonlydoweneedtomapthedatafromMotorcycletoMotorcycleData,butwealsomighthavenumerouscollectionsthatneedtobepopulatedfordropdownlists,etc.Ifweputallofthiscodeinthecontroller,thenthecontrollerwillquicklyendupwithalotofcodededicatedjusttobuildingtheviewmodelwhichisnotdesirableaswewanttokeepourcontrollersthin.Therefore,wecanintroducea“builder”classthatisconcernedwithbuildingtheviewmodel.
publicclassMotorcycleViewModelBuilder
[code]{
privateIMotorcycleRepositorymotorcycleRepository;
publicMotorcycleViewModelBuilder(IMotorcycleRepositoryrepository)
{
this.motorcycleRepository=repository;
}
publicMotorcycleViewModelBuild()
{
//codeheretofullybuildtheviewmodel
//withmethodsintherepository
}
}
[/code]
Thisallowsourcontrollercodetolooksomethinglikethis:
publicActionResultEdit(intid)
[code]{
varviewModelBuilder=newMotorcycleViewModelBuilder(this.motorcycleRepository);
varmotorcycleViewModel=viewModelBuilder.Build();
returnthis.View();
}
[/code]
Ourviewscanlookprettymuchthesameaspattern#2butnowwehavethecomfortofknowingthatwe’reonlypassinginthedatatotheviewthatweneed–nomore,noless.Whentheformispostedback,ourcontroller’sSave()methodcannowlooksomethinglikethis:
publicActionResultSave([Bind(Prefix="Motorcycle")]MotorcycleDatamotorcycleData)
[code]{
varmapper=newMotorcycleMapper(motorcycleData);
Motorcyclemotorcycle=mapper.Map();
this.motorcycleRepository.Save(motorcycle);
returnthis.RedirectToAction("Index");
}
[/code]
Conceptually,thisimplementationisverysimilartoBogard’spostbutwithouttheAutoMapattribute.TheAutoMapattributeallowsustokeepsomeofthiscodeoutofthecontrollerwhichcanbequitenice.Oneadvantagetonotusingitisthatthecodeinsidethecontrollerclassismoreobviousandexplicit.Additionally,ourbuilderandmapperclassesmightneedtobuildtheobjectsfrommultiplesourcesandrepositories.Internallyinourmapperclasses,youcanstillmakegreatuseoftoolslikeAutoMapper.
Inmanycomplexreal-worldcases,somevariationofpattern#3isthebestchoiceasitaffordsthemostflexibilitytothedeveloper.
Considerations
Howdoyoudeterminethebestapproachtotake?Herearesomeconsiderationstokeepinmind:
CodeRe-use–Certainlypatterns#1and#2lendthemselvesbesttocodere-useasyouarebindingyourviewsdirectlytoyourdomainmodelobjects.Thisleadstoincreasedcodebrevityasmappinglayersarenotrequired.However,ifyourviewconcernsdifferfromyourdomainmodel(whichtheyoftenwill)options#1and#2begintobreakdown.
Impedancemismatch–Oftenthereisanimpedancemismatchbetweenyourdomainmodelandtheconcernsofyourview.Inthesecases,option#3givesthemostflexibility.
MappingLayer–Ifcustomviewentitiesareusedasinoption#3,youmustensureyouestablishapatternforamappinglayer.Althoughthismeansmorecodethatmustbewritten,itgivesthemostflexibilityandtherearelibrariesavailablesuchasAutoMapperthatmakethiseasiertoimplement.
Validation–Althoughtherearemanywaystoperformvalidation,oneofthemostcommonistouselibrarieslikeDataAnnotations.Althoughtypicalvalidations(e.g.,requiredfields,etc.)willprobablybethesamebetweenyourdomainmodelsandyourviews,notallvalidationwillalwaysmatch.Additionally,youmaynotalwaysbeincontrolofyourdomainmodels(e.g.,insomeenterprisesthedomainmodelsareexposedviaservicesthatUIdeveloperssimplyconsume).Sothereisalimittohowyoucanassociatevalidationswiththoseclasses.Yes,youcanuseaseparate“metadata”classtodesignatevalidationsbutthisduplicatessomecodesimilartohowaviewmodelentityfromoption#3wouldanyway.Therefore,option#3givesyoutheabsolutemostcontroloverUIvalidation.
Conclusion
Thefollowinghasbeenasummaryofseveralofthepatternsthathaveemergedindealingwithviewmodels.AlthoughthesehaveallbeeninthecontextofASP.NETMVC,theproblemwithhowbesttodealwithviewmodelsisalsoanissuewithotherframeworkslikewebformsaswell.Ifyouareabletobinddirectlytodomainmodelinsimplecases,thatisthesimplestandeasiestsolution.However,asyourcomplexitygrows,havingdistinctviewmodelsgivesyouthemostoverallflexibility.
相关文章推荐
- Asp.Net MVC之ViewData字典与ViewModel模式
- ASP.NET Model View Controller (MVC) Framework
- 从零开始学习 ASP.NET MVC 1.0 (四) View/Model 全解
- Asp.net mvc ViewModel
- Asp.Net MVC中Controller与View之间传递的Model
- 在ASP.NET MVC中使用Knockout实践02,组合View Model成员、Select绑定、通过构造器创建View Model,扩展View Model方法
- 敏捷开发中asp.net MVC的开发次序感受(先开发View?先开发Model?先开发Controller!)
- asp.net MVC: PagedList + View Model
- 敏捷开发中asp.net MVC的开发次序感受(先开发View?先开发Model?先开发Controller!)
- 从零开始学习 ASP.NET MVC 1.0 (四) View/Model 全解 【转】
- 从零开始学习 ASP.NET MVC 1.0 (四) View/Model 全解
- ASP.NET MVC传递Model到视图的多种方式总结(二)__关于ViewBag、ViewData和TempData的实现机制与区别
- 从零开始学习 ASP.NET MVC 1.0 (四) View/Model 全解
- 敏捷开发中asp.net MVC的开发次序感受(先开发View?先开发Model?先开发Controller!)
- Asp.Net MVC之ViewData字典与ViewModel模式
- 敏捷开发中asp.net MVC的开发次序感受(先开发View?先开发Model?先开发Controller!)
- ASP.NET MVC中Model View Controller的开发顺序
- ASP.NET MVC - The view must derive from WebViewPage, or WebViewPage<TModel>
- HTML5, jQuery Mobile 和 ASP.NET MVC 4——在模型和控制器之间使用ViewModel
- 【jqGrid for ASP.NET MVC Documentation】.学习笔记.2.jqGrid Model-View-Controller 分离