Using FSharp with OrchardCMS

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