ASP.NET MVC with NHaml - F# Edition
2010-05-21 17:42
253 查看
AspartofsomeofmyadventureswithF#,I'veseenalotofinterestingthingscomingfromotherswithregardstoSharePoint,ASP.NETandothertechnologies.ThishadmethinkingofanypossibilitiesandramificationsofusingF#withASP.NETMVC.Wasitpossible,andbetterquestion,whatmightmakesomeoneusethisovertheirexistingtoolsets.Thosearesomeofthequestionstoexplore.But,inthemeantime,let'stakethejourneyofF#andASP.NETMVC.
F#September2008CTP
ASP.NETMVCPreview5
MVCContribforASP.NETMVCPreview5
Sidebyside,Ithinkit'seasiertofirstcreateasampleC#ASP.NETMVCproject,soit'seasytocutandpastetheconfigurationfileinformation.WhatworksbetteristoopentheNHamlViewEnginesamplefromMVCContrib.Also,createastandardF#library,andinmycase,IcalleditMvcFSharp.
Ithenaddreferencestothefollowingassemblies:
Microsoft.Web.Mvc.dll
MvcContrib.dll
MvcContrib.NHamlViewEngine.dll
System.Web.dll
System.Web.Abstractions.dll
System.Web.Extensions.dll
System.Web.Mvc.dll
System.Web.Routing.dll
SincetheF#projectsdonotsupportcreatingfolders,thisnextsteprequiressomeVisualNotepadsupport.
Inthisactualcase,thesearetherealfileslistedforourfirstF#ASP.NETMVCapplication.OnceyoureloadtheprojectfilefromVisualStudio,youshouldbereadytogo.OnethingtokeepinmindwithyourprojectcompilationisthatinF#,orderoffilesdoesmatter.Anyfurtherworkwhereordermaymattercouldforceyoutochangetheprojectfileconfigurationwithnotepadonceagain.Oncetheprojectfilehasbeenmodified,let'smoveontomodifyingtheweb.configtoreflectusingF#asacompiler.
Now,addthenhamlViewEngineinformationinasfollows.NotethatI'maddingsomereferencestoF#.ThereasonbeingisthatshouldIreturnatypethatisF#specificsuchasalist,seqorotherwise,NHamlwillnotbeabletoreferenceproperlywithoutaccesstotheFSharp.Core.dll.
Inorderforustocompileourdefault.aspx.fsandglobal.asax.fsfile,weneedtoaddsupportfortheF#compilerinourweb.config.AddthefollowingsectionofXMLtoyourcompilerssection,rightnexttoyourC#compilerregistration.
WenowhavetheF#ASP.NETCodeProviderinstalledinourweb.config,soourfocusnowshiftstoproperregistrationinourglobal.asax.fsanddefault.aspx.fs.
Ifyoufollowedtheprojectstructurefromabove,opentheglobal.asax.fsfileandmodifyittolooklikethis.
#light
namespaceMvcFSharp
openSystem.Web.Mvc
openSystem.Web.Routing
openMvcContrib.ControllerFactories
openMvcContrib.NHamlViewEngine
typeMvcConstraint2={action:string;id:string}
typeMvcConstraint3={controller:string;action:string;id:string}
typeMvcApplication()=
inheritSystem.Web.HttpApplication()
staticmemberRegisterRoutes(routes:RouteCollection)=
routes.Add(
newRoute("{controller}.mvc/{action}/{id}",
newMvcRouteHandler(),
Defaults=newRouteValueDictionary({newMvcConstraint2withaction="index"andid=""})))
routes.Add(
newRoute("Default.aspx",
newMvcRouteHandler(),
Defaults=newRouteValueDictionary({controller="Home";action="index";id=""})))
memberx.Application_Start()=
MvcApplication.RegisterRoutesRouteTable.Routes
ViewEngines.Engines.Add(newNHamlViewFactory())
Asyoucanseefromabove,Ihadtoaddtworecordtypes,calledMvcConstraint2andMvcConstraint3.ThereasonbeingisthatF#doesnotdoanonymoustypesasC#does.Instead,youdefineasimplerecordtypetoholdthedataasneeded.Sincetherearetwodifferentneeds,onewithtwofieldsandonewiththree,thereisaneedtodefinetwoseparateinstances.MuchasyouwouldintheC#code,theregistrationshouldnotlookallthatdifferent.But,IkepttheRegisterRoutesfunctionsothatIcantestmyroutesinaniceTDDfashion.
Movingontothedefault.aspxfile,modifythedefault.aspxtolooklikethefollowing:
Oncethatiscomplete,moveontothedefault.aspx.fsfile.Itshouldlooklikethefollowing:
#light
namespaceMvcFSharp
openSystem
openSystem.Web.UI
type_Default()=
inheritPage()
memberx.Page_Load(sender:obj,e:EventArgs)=
x.Response.Redirect("~/Home.mvc/Index")
Wenowhaveabasicsetupinwhichtobuilduponforourapplication.Nowwecanconcentrateonthemodels,controllersandviews.
Let'screateonetoholdjustsomenumberstodisplayonthescreen.Iffollowingtheaboveprojectstructure,yourListViewData.fsshouldlooklikethefollowing:
#light
namespaceMvcFSharp.Models
typeListViewData={Numbers:seq<int>}
Done!Nowthatwaseasy!Movingontothecontroller...
#light
namespaceMvcFSharp.Controllers
openMvcFSharp.Models
openSystem.Web.Mvc
[<HandleError>]
typeHomeController()=
inheritController()
memberx.Index()=
x.View("Index")
memberx.About()=
x.View("About")
memberx.Numbers()=
letviewData={Numbers={1..10}}
x.View("Numbers",viewData)
Inthisexample,Ididnothingmorethanjusttellthesystemtorendertheview.Eachtime,it'sbestthatyoucastittotheappropriatereturntypemuchasIdidabove.InthecaseoftheNumbersfunction,Iwantedtocreateasetofnumberstorendertothescreen,soIcreatemynewListViewDatawithmynumbersset.Then,Ipassthattotheviewtorender.
%h2WelcometomyASP.NETMVCApplicationusingNHaml!
=Html.ActionLink("AboutUs","About","Home")
=Html.ActionLink("Data","Numbers","Home")
Let'smoveontoourNumbers.hamlfilewhichwillusethedatathatIpopulatedfromourHomeController.Itshouldlooklikethefollowing:
%h2DatafromController
%ul
-foreach(varninViewData.Model.Numbers)
%li=n
Therestshouldstaythesamemuchasbefore.AsIsaid,Ionlywantedtotryoutafewfeaturesbeforegoingintoafullfledgedapplication.
.png]
Ouraboutpagewilllooklikethefollowing:
.png]
Andlastly,ournumberspagewilldisplaythenumbersfrom1-10inanunorderedlist:
.png]
So,asyoucansee,wenowdisplayourdatafromourF#controllerandF#models.But,isthatallofourstorytotell?Ofcoursenot?Ithinkit'simportanttoemphasizeTDDwiththisapproach.ThisworksnodifferentthanitwouldinC#,quitefrankly.
FsTestwhichcreatesaDSLovertheassertionsyntax.Thisallowsmetomorenaturallytestusingfunctionalprogrammingstrengths.Usingthis,I'mabletotestallofmycodemuchasyouwouldinyourC#solution.
Let'sfirststartwithourMvcApplicationtests.Let'sgothroughoneoftheteststhatIdidearlierinregardstomyNumbersfunction.
#light
namespaceMvcFSharp.Tests.Controllers
openFsxUnit.Syntax
openMvcFSharp.Controllers
openSystem.Web.Mvc
moduleNumbersFacts=
[<Concern>]
letsets_model()=
//Arrange
letcontroller=newHomeController()
//Act
letresult=controller.Numbers()
//Assert
result.ViewData.Model|>shouldbeNonNull
ThisisjustoneinthenumberofunitteststhatIdefinedforthisapplication.Asyoucansee,it'squiteeasytouseFsxUnitinusingtheAAAsyntax.
I'vemadetheprojectavailablehere.
GettingStarted
First,let'scoverwhatittakestogetF#toworkwithASP.NETMVC.Therequireddownloadsare:Sidebyside,Ithinkit'seasiertofirstcreateasampleC#ASP.NETMVCproject,soit'seasytocutandpastetheconfigurationfileinformation.WhatworksbetteristoopentheNHamlViewEnginesamplefromMVCContrib.Also,createastandardF#library,andinmycase,IcalleditMvcFSharp.
Ithenaddreferencestothefollowingassemblies:
Microsoft.Web.Mvc.dll
MvcContrib.dll
MvcContrib.NHamlViewEngine.dll
System.Web.dll
System.Web.Abstractions.dll
System.Web.Extensions.dll
System.Web.Mvc.dll
System.Web.Routing.dll
SincetheF#projectsdonotsupportcreatingfolders,thisnextsteprequiressomeVisualNotepadsupport.
ModifyingtheProjectFile
First,createtheModels,Controllers,ContentandViewsdirectoriesthroughWindowsExplorer.Createdummyfilesineachfolderisprobablytheeasiestthingtodo.Whenyouaredone,yourprojectfilecontainthis:<ItemGroup> <CompileInclude="Models\ListViewData.fs"/> <CompileInclude="Controllers\HomeController.fs"/> <CompileInclude="Default.aspx.fs"> <DependentUpon>Default.aspx</DependentUpon> <SubType>ASPXCodeBehind</SubType> </Compile> <CompileInclude="Global.asax.fs"> <DependentUpon>Global.asax</DependentUpon> </Compile> <ContentInclude="Default.aspx"/> <ContentInclude="Global.asax"/> <ContentInclude="Content\Site.css"/> <ContentInclude="Content\MicrosoftAjax.js"/> <ContentInclude="Content\MicrosoftAjax.debug.js"/> <ContentInclude="Content\MicrosoftMvcAjax.js"/> <ContentInclude="Content\MicrosoftMvcAjax.debug.js"/> <ContentInclude="Views\Home\About.haml"/> <ContentInclude="Views\Home\Index.haml"/> <ContentInclude="Views\Home\Numbers.haml"/> <ContentInclude="Views\Masters\Application.haml"/> <ContentInclude="Web.config"/> </ItemGroup>
Inthisactualcase,thesearetherealfileslistedforourfirstF#ASP.NETMVCapplication.OnceyoureloadtheprojectfilefromVisualStudio,youshouldbereadytogo.OnethingtokeepinmindwithyourprojectcompilationisthatinF#,orderoffilesdoesmatter.Anyfurtherworkwhereordermaymattercouldforceyoutochangetheprojectfileconfigurationwithnotepadonceagain.Oncetheprojectfilehasbeenmodified,let'smoveontomodifyingtheweb.configtoreflectusingF#asacompiler.
ModifyingtheConfiguration
ThereareseveralitemsweneedtoaddinordertogetbothNHamlandF#toworkasoneinsideourweb.config.AsIsaidearlier,it'sprobablyeasiesttocopy/pastesomebasicinformationfromtheNHamlViewEnginesamplefromMVCContribtosaveyourselffromhavingtoreferenceadditionalthings.First,let'saddNHamlsupporttoourprojectfile.WeneedtomodifytheconfigSectionsareatoaddnhamlViewEnginesupport.AddthefollowingtexttoyourconfigSections:<configSections>
<sectionname="nhamlViewEngine"
type="MvcContrib.NHamlViewEngine.Configuration.NHamlViewEngineSection,
MvcContrib.NHamlViewEngine,
Version=0.0.1.159,
Culture=neutral,
PublicKeyToken=null"/>
Now,addthenhamlViewEngineinformationinasfollows.NotethatI'maddingsomereferencestoF#.ThereasonbeingisthatshouldIreturnatypethatisF#specificsuchasalist,seqorotherwise,NHamlwillnotbeabletoreferenceproperlywithoutaccesstotheFSharp.Core.dll.
<nhamlViewEngineproduction="false">
<views>
<assemblies>
<addassembly="FSharp.Core,
Version=1.9.6.2,
Culture=neutral,
PublicKeyToken=a19089b1c74d0809"/>
</assemblies>
<namespaces>
<addnamespace="Microsoft.FSharp.Core"/>
</namespaces>
</views>
</nhamlViewEngine>
Inorderforustocompileourdefault.aspx.fsandglobal.asax.fsfile,weneedtoaddsupportfortheF#compilerinourweb.config.AddthefollowingsectionofXMLtoyourcompilerssection,rightnexttoyourC#compilerregistration.
<compilerlanguage="F#;f#;fs;fsharp"
extension=".fs"
warningLevel="4"
type="Microsoft.FSharp.Compiler.CodeDom.FSharpAspNetCodeProvider,
FSharp.Compiler.CodeDom,Version=1.9.6.2,
Culture=neutral,
PublicKeyToken=a19089b1c74d0809">
<providerOptionname="CompilerVersion"value="v3.5"/>
<providerOptionname="WarnAsError"value="false"/>
</compiler>
WenowhavetheF#ASP.NETCodeProviderinstalledinourweb.config,soourfocusnowshiftstoproperregistrationinourglobal.asax.fsanddefault.aspx.fs.
ModifyingtheDefaults
Nowweturnourattentiontothetwodefaultsforourapplication,theglobal.asaxandthedefault.aspx.First,modifytheglobal.asaxtoindicatethefollowing:<%@ApplicationCodeBehind="Global.asax.fs"Inherits="MvcFSharp.MvcApplication"Language="F#"%>
Ifyoufollowedtheprojectstructurefromabove,opentheglobal.asax.fsfileandmodifyittolooklikethis.
#light
namespaceMvcFSharp
openSystem.Web.Mvc
openSystem.Web.Routing
openMvcContrib.ControllerFactories
openMvcContrib.NHamlViewEngine
typeMvcConstraint2={action:string;id:string}
typeMvcConstraint3={controller:string;action:string;id:string}
typeMvcApplication()=
inheritSystem.Web.HttpApplication()
staticmemberRegisterRoutes(routes:RouteCollection)=
routes.Add(
newRoute("{controller}.mvc/{action}/{id}",
newMvcRouteHandler(),
Defaults=newRouteValueDictionary({newMvcConstraint2withaction="index"andid=""})))
routes.Add(
newRoute("Default.aspx",
newMvcRouteHandler(),
Defaults=newRouteValueDictionary({controller="Home";action="index";id=""})))
memberx.Application_Start()=
MvcApplication.RegisterRoutesRouteTable.Routes
ViewEngines.Engines.Add(newNHamlViewFactory())
Asyoucanseefromabove,Ihadtoaddtworecordtypes,calledMvcConstraint2andMvcConstraint3.ThereasonbeingisthatF#doesnotdoanonymoustypesasC#does.Instead,youdefineasimplerecordtypetoholdthedataasneeded.Sincetherearetwodifferentneeds,onewithtwofieldsandonewiththree,thereisaneedtodefinetwoseparateinstances.MuchasyouwouldintheC#code,theregistrationshouldnotlookallthatdifferent.But,IkepttheRegisterRoutesfunctionsothatIcantestmyroutesinaniceTDDfashion.
Movingontothedefault.aspxfile,modifythedefault.aspxtolooklikethefollowing:
<%@PageLanguage="F#"AutoEventWireup="true"CodeBehind="Default.aspx.fs"Inherits="MvcFSharp._Default"%>
Oncethatiscomplete,moveontothedefault.aspx.fsfile.Itshouldlooklikethefollowing:
#light
namespaceMvcFSharp
openSystem
openSystem.Web.UI
type_Default()=
inheritPage()
memberx.Page_Load(sender:obj,e:EventArgs)=
x.Response.Redirect("~/Home.mvc/Index")
Wenowhaveabasicsetupinwhichtobuilduponforourapplication.Nowwecanconcentrateonthemodels,controllersandviews.
CreatingtheModel
IwantjustabasicmodeltoshowthatcreatingconciseandcompactmodelsisrelativelysimpleusingF#.AsIdidfortheMvcConstraintrecordtypesabove,Icaneasilyapplytomymodel.Sometimes,ourmodelsmaybenothingmorethanjustawriteonceoperation,sosimpleimmutablerecordtypessuffice.Othertimes,wemayneedtomakesomeofthefieldsmutable.But,that'sthebeautyofF#,isthatitallowsustodoboth.Let'screateonetoholdjustsomenumberstodisplayonthescreen.Iffollowingtheaboveprojectstructure,yourListViewData.fsshouldlooklikethefollowing:
#light
namespaceMvcFSharp.Models
typeListViewData={Numbers:seq<int>}
Done!Nowthatwaseasy!Movingontothecontroller...
CreatingtheController
Wehavethemodelsnowdefined,solet'smoveontothecontrollers.Ionlywantonecontrollerduringthisexample,inthiscasetheHomeController.fs.Let'ssayIhavethreeviewsIwanttoworkwith,theIndex,AboutandData.Definingsuchacontrollerisquitesimple.Itshouldlooksomethinglikethis:#light
namespaceMvcFSharp.Controllers
openMvcFSharp.Models
openSystem.Web.Mvc
[<HandleError>]
typeHomeController()=
inheritController()
memberx.Index()=
x.View("Index")
memberx.About()=
x.View("About")
memberx.Numbers()=
letviewData={Numbers={1..10}}
x.View("Numbers",viewData)
Inthisexample,Ididnothingmorethanjusttellthesystemtorendertheview.Eachtime,it'sbestthatyoucastittotheappropriatereturntypemuchasIdidabove.InthecaseoftheNumbersfunction,Iwantedtocreateasetofnumberstorendertothescreen,soIcreatemynewListViewDatawithmynumbersset.Then,Ipassthattotheviewtorender.
CreatingtheViews
AsI'vestatedbefore,I'minterestedinfollowingtheexamplefromMVCContribfortheNHamlViewEngineasmuchaspossible.So,theviewslook100%liketheydofromtheproject,exceptformynumbers.hamlfile.Let'slookattheviewsthatmatter.First,theindex.hamlfile.%h2WelcometomyASP.NETMVCApplicationusingNHaml!
=Html.ActionLink("AboutUs","About","Home")
=Html.ActionLink("Data","Numbers","Home")
Let'smoveontoourNumbers.hamlfilewhichwillusethedatathatIpopulatedfromourHomeController.Itshouldlooklikethefollowing:
%h2DatafromController
%ul
-foreach(varninViewData.Model.Numbers)
%li=n
Therestshouldstaythesamemuchasbefore.AsIsaid,Ionlywantedtotryoutafewfeaturesbeforegoingintoafullfledgedapplication.
TryingitOut
Oncetheapplicationisbuilt,wecanthencreatethevirtualdirectoryinIIStohostourapplication.Oncethatiscomplete,launchingthebrowserwillgiveusthisforourIndexview.Ouraboutpagewilllooklikethefollowing:
Andlastly,ournumberspagewilldisplaythenumbersfrom1-10inanunorderedlist:
So,asyoucansee,wenowdisplayourdatafromourF#controllerandF#models.But,isthatallofourstorytotell?Ofcoursenot?Ithinkit'simportanttoemphasizeTDDwiththisapproach.ThisworksnodifferentthanitwouldinC#,quitefrankly.
TestDrivingourSolution
MuchlikewhenyoucreateanewASP.NETMVCproject,itwillbydefaulthelpyoucreateasetofunittestsusingthexUnitframeworkofyourhoice.Inthiscase,mydefaultisxUnit.net.Let'stalkabouttestinghereonceagain.AsI'vestatedbefore,IcreatedanoverallprojectcalledLet'sfirststartwithourMvcApplicationtests.Let'sgothroughoneoftheteststhatIdidearlierinregardstomyNumbersfunction.
#light
namespaceMvcFSharp.Tests.Controllers
openFsxUnit.Syntax
openMvcFSharp.Controllers
openSystem.Web.Mvc
moduleNumbersFacts=
[<Concern>]
letsets_model()=
//Arrange
letcontroller=newHomeController()
//Act
letresult=controller.Numbers()
//Assert
result.ViewData.Model|>shouldbeNonNull
ThisisjustoneinthenumberofunitteststhatIdefinedforthisapplication.Asyoucansee,it'squiteeasytouseFsxUnitinusingtheAAAsyntax.
WrappingitUp
Asyoucansee,gettingF#toworkwithASP.NETMVCwasn'tabsolutelytrivial.Butoncetheoverallprojectskeletonisdefined,modifyingittofityourapplicationiseasier.But,thequestioncomesup,whybotherdoingthis?IknowI'mgoingtogetthatquestionalot.Well,firstoff,itwasachallengetomyself.But,secondly,I'mabletousetheconciseF#syntaxtoexpresscontrollersandmodelsquiteeasilywithoutmuchpompandcircumstance.Maybeahybridapproachmayworkbetter?MaybeF#asaviewenginemayyieldbetterresults?Anyhow,feelfreetopickthroughthissampleandletmeknowyourthoughts.I'vemadetheprojectavailable
相关文章推荐
- Using ASP.NET MVC with Different Versions of IIS
- using Silverlight 4 in an ASP.NET MVC 3 application and accessing data with JSON
- ASP.NET MVC with Entity Framework and CSS一书翻译系列文章之第一章:创建基本的MVC Web站点
- Asp.net MVC 4 application with Flexigrid, jQuery UI, and jQuery validation
- [ASP.NET MVC] Model Binding With NameValueCollectionValueProvider
- asp.net mvc working with ajax
- Mobile enabled web apps with ASP.NET MVC 3
- Hierarchical Treeview with ASP.NET MVC & jQuery
- Server-Side Paging with the Entity Framework and ASP.NET MVC 3
- [译]Pro ASP.NET MVC 3 Framework 3rd Edition (Chapter 20 JQuery) 5.Using jQuery Events 使用jQuery事件
- Getting started with ASP.NET Core MVC and Visual Studio
- http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application
- [转]Using the Repository Pattern with ASP.NET MVC and Entity Framework
- ASP.NET MVC with Entity Framework and CSS一书翻译系列文章之第二章:利用模型类创建视图、控制器和数据库
- ASP.NET MVC with Entity Framework and CSS一书翻译系列文章之第三章:搜索、高级过滤和视图模型
- Active Directory Authentication in ASP.NET MVC 5 with Forms Authentication and Group-Based Authorization
- ASP.NET MVC – Create easy REST API with JSON and XML(转)
- Web API with ASP.NET Core 1.0 MVC
- ASP.NET MVC 3: Layouts with Razor