Using FSharp with OrchardCMS
Introduction
This post describes how to use FSharp and Orchard together. If you want to know why to use FSharp, here you can find a lot of reasons (http://fsharpforfunandprofit.com/why-use-fsharp/).
In this post you won't learn how to use FSharp or Orchard, it only will show you how to use them together without changing anything in the core of Orchard.
Configuring the environment
The first thing that we have to do is to download the source of the latest stable Orchard version (at this moment 1.7.2) [https://orchard.codeplex.com/downloads/get/759691]. We just have to unzip the file and open the sln file with Visual Studio. If we run it now, Orchard should launch its pretty simple configuration page. Just fill the form (choose the SQLCompact database and the Default recipe).
When Orchard shows the HomePage just go back to Visual Studio and add a FSharpLibrary project to the solution. Call it FSharpModule. The location of the project should be on src/Orchard.Web/Modules.
Now is time to configure our project to work with Orchard, go to the Properties of the project and on Build->Output path choose Orchard.Web/bin directory. This way Orchard will rediscover your module and its dependencies (such as FSharp.Core) each time that you compile it. Note that Dynamic Compilation is not working and every time that you have to change your Module you have to compile it again, but since the assembly is on Orchard.Web/Bin directory, Orchard will notice that something changed and it will pick the assembly again.
We also have to change the .NET target framework to 4.0. and the FSharp target runtime to 3.0. The next step is to create the Module.txt file. It should look something like this:
Name: FSharpModule
AntiForgery: enabled
Author: Juan M Gómez
Website: http://orchardproject.net
Version: 1.7.2
OrchardVersion: 1.7.2
Description: This is a sample of module in fsharp
The file is mandatory, Orchard will explore the Modules subfolders looking for that file in each module. The next step is to add the references:
Orchard.Core
Orchard.Framework
System.Web
System.Web.Mvc
System.Xml
System.Xml.Linq
System.ComponentModel.DataAnnotations
FluentNHibernate
NHibernate
We also have to add the nuget package FSharp.Dynamic. We will need it to play nicely with Dynamic objects.
Disabling "Edit and continue" on Orchard.Web->options->web is also helpful to prevent restarting iis every time that you want to stop the debugger.
Hello World Orchard
Next step is to write the code, I'm writing it on Library1.fs. This is a simple controller with a simple model.
namespace FSharpModule
open System.Web.Mvc
open Orchard.Themes
type Salute() =
member this.SayHello = "Hello from Fsharp"
[<Themed>]
type FSharpController() =
inherit Controller()
member this.Index() =
this.View(Salute())
By marking the controller with the Themed attribute orchard will inject our View to the current theme. If we don't use it, our plain view will be displayed.
For creating our views, since FSharp projects don't support directories by default, you can copy->paste the View folder from another module to the directory and it will be created on Modules/FSharpModule/Views which is where orchard will look for Views. But I recommend to create the file structure outside of VisualStudio (By the way you can also create the Index view on the View directory of your theme). So, on the View directory you must have the web.config file and the Index.cshtml file
@Model.SayHello
Now it's time to activate it, compile the library and go back to the browser (or run orchard again if you closed it), you will see our module on /admin -> Modules -> feature. Activate it. The next step is to navigate to the url of our controller, since each module in orchard can be regarded as a mvc area, you have to put the module name on the url, in our case: ~/ModuleFSharp/FSharp . Notice that you can change that route by implementing IRouteProvider, but that's out of the scope of this post.
So here it is:
A simple ContentPart
Now it’s time to truly integrate FSharp with Orchard by creating a ContentPart and a ContentItem. I'm going to base this example on the MapPart tutorial from the Orchard documentation (http://docs.orchardproject.net/Documentation/Writing-a-content-part).
I will start by creating the Model, to keep the post as simple as possible I will do everything on the same file, except the Models which have to be placed under the FSharpModule.Models namespace, because Orchard will look into it to discover Records. Be aware that in FSharp the order of the files matters, so our Models file should be before our Library1 file.
We should add these namespaces to our Models file
namespace FSharpModule.Models
open Orchard.ContentManagement
open Orchard.ContentManagement.Records
open System.ComponentModel.DataAnnotations
An for the Library.1fs file
namespace FSharpModule
open System.Web.Mvc
open Orchard.Themes
open Orchard.ContentManagement
open Orchard.ContentManagement.Records
open System.ComponentModel.DataAnnotations
open Orchard.ContentManagement.Handlers
open Orchard.Data
open Orchard.ContentManagement.Drivers
open EkonBenefits.FSharp.Dynamic
open Orchard.Data.Migration
open Orchard.ContentManagement.MetaData
open Orchard.ContentManagement.MetaData.Builders
open Orchard.Core.Contents.Extensions
open System.Data
open FSharpModule.Models
Notice that this is not the way of organizing the code in fsharp, it is just to keep it as short as possible.
So we're going to start with our model, in our Models.fs file we're going to write our MapRecord:
type DVA = DefaultValueAttribute
type MapRecord() =
inherit ContentPartRecord()
[<DVA(false)>] valmutableprivate lat : double
[<DVA(false)>] valmutableprivate lon : double
abstract Latitude : double with get,set
override this.Latitude with get() = this.lat
and set(v) = this.lat <- v
abstract Longitude : double with get,set
override this.Longitude with get() = this.lon
and set(v) = this.lon <- v
This can be a bit painful, since implementing a virtual property with back fields in FSharp, it generates fields in runtime (see here for an example https://gist.github.com/jmgomez/8476420) the NHibernate proxy validation will fail. So we have to tell NHibernate that we don't want to use the proxy validator, we can do it just implementing ISessionConfigurationEvents, back on Library1.fs
type SessionConfiguration() =
interface ISessionConfigurationEvents with
member this.Created(cfg,defaultModel) =
cfg.ExposeConfiguration(fun c->c.Properties.Add("use_proxy_validator", "false") |>ignore)|>ignore
ignore()
member this.Building(cfg) =
ignore()
member this.Prepared(cfg) =
ignore()
member this.Finished(cfg) =
ignore()
member this.ComputingHash(hash) =
ignore()
In our Models.fs again, MapPart
type MapPart() =
inherit ContentPart<MapRecord>()
[<Required>]
member this.Latitude
with get() = base.Record.Latitude
and set(v) = base.Record.Latitude <- v
[<Required>]
member this.Longitude
with get() = base.Record.Longitude
and set(v) = base.Record.Longitude <- v
Notice the beauty of the type inference on the properties, we don’t have to tell FSharp that the properties are double.
Now it’s time to make the Handler, so go back to Library.fs.
It will be needed to tell Orchard how to store the content.
type MapHandler(repository:IRepository<MapRecord>) =
inherit ContentHandler()
do
base.Filters.Add(StorageFilter.For(repository)) |> ignore
The next step is to do our Driver, Orchard uses drivers to create the template which will render the Display (for the front-end) and Editors (dashboard) methods. For each content item Orchard will iterate over its parts composing the view. This process is dynamic heavy, this is why we installed the FSharp.Dynamic library from nuget.
type MapDriver() =
inherit ContentPartDriver<MapPart>()
override this.Prefix with get() = "MapPart"
override this.Display(part,displayType,shapeHelper) =
this.ContentShape("Parts_Map",fun()-> shapeHelper?Parts_Map(part))
:> DriverResult
//GET
override this.Editor(part,shapeHelper) =
let editor = shapeHelper?EditorTemplate()
editor?TemplateName <- "Parts/Map"
editor?Model <- part
editor?Prefix <- this.Prefix
this.ContentShape("Parts_Map_Edit",fun()->editor):> DriverResult
//POST
override this.Editor(part,updater,shapeHelper) =
updater.TryUpdateModel(part,this.Prefix,null,null) |> ignore
this.Editor(part,shapeHelper)
Since there is no way to use Named Parameters dynamically on FSharp, we have to add the parameter to the shape in each line. Not a big deal.
Now it’s time to do our migration, the migration is where our scheme is defined.
We're going to go a bit further and do a Widget with our MapPart. So this is the Migration file:
type Migration() =
inherit DataMigrationImpl()
member this.Create()=
this.SchemaBuilder.CreateTable("MapRecord",fun table->
table.ContentPartRecord().Column<double>("Latitude")
|> fun t->t.Column<double>("Longitude")
|>ignore)
|>ignore
this.ContentDefinitionManager.AlterPartDefinition("MapPart",fun cfg->
cfg.Attachable()
|>ignore)
|>ignore
this.ContentDefinitionManager.AlterTypeDefinition("MapWidget",fun cfg->
cfg.WithPart("MapPart").WithPart("WidgetPart")
|> fun t->t.WithPart("CommonPart").WithSetting("Stereotype","Widget")
|>ignore)
|>ignore
1
If we start the site again, you will see that the database is modified with our MapRecord table and the rest of values of our Migration.
Adding the Views
We should create the following file structure under Modules/FSharpModule/Views
Parts/Map.cshtml
EditorTemplates/Parts/Map.cshtml
The Parts/Map.cshtml file:
<img alt="Location" border="1" src="http://maps.google.com/maps/api/staticmap?
&zoom=14
&size=256x256
&maptype=roadmap
&markers=color:blue|@Model.Latitude,@Model.Longitude
&sensor=false" />
The Editor/Parts/Map.cshtml file
@model FSharpModule.Models.MapPart
<fieldset>
<legend>Map Fields</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Latitude)
</div>
<div class="editor-field">
@Html.TextBoxFor(model => model.Latitude)
@Html.ValidationMessageFor(model => model.Latitude)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Longitude)
</div>
<div class="editor-field">
@Html.TextBoxFor(model => model.Longitude)
@Html.ValidationMessageFor(model => model.Longitude)
</div>
</fieldset>
Now we just have to tell Orchard where our mappart should be placed inside of our ContentItem, so we just add a new file called placement.info and to put the following code:
<Placement>
<Place Parts_Map="Content:10"/>
<Place Parts_Map_Edit="Content:7.5"/>
</Placement>
Now it’s time to test that everything works as expected. Start the site again and go to admin->widget->content->add->Map Widget , fill the fields and save it. Navigate to the home and you should see the map there.
And that’s all.
You can download the source here: https://github.com/jmgomez/FSharpModule