Chapter 21. ASP.NET MVC

WHAT'S IN THIS CHAPTER?

  • Building websites

  • Integrating C# views with F# controllers and models

  • Keeping concerns properly separated

  • Understanding the MVC pattern applied to F#

While F# is a strong contender in many areas of the development stack, it has not been traditionally thought of as a great candidate for development of code on the UI layer. This is especially true for platforms like WinForms, which promote a sense of statefulness that flows through not just the UI, but through the object model that a UI might be bound to. Most UI technologies of the WinForm era (including ASP.NET WebForms) are tied to the idea of mutating control objects as a core mechanism by which you promote interaction between a presentation object (MVP pattern) or ViewModel (MVVM pattern), and mutable UI objects (like controls with a .Text property). For such a stateful model, F# is merely an average OO language on the .NET framework, with no real compelling application besides being yet another language option.

For other models, however, that depend less on state maintenance — such as MVC frameworks — F# is far more compelling. In this chapter, you learn how to use F# with the ASP.NET MVC framework. We demonstrate an example of writing an ASP.NET MVC application that demonstrates how to use F# to simplify web development.

OVERVIEW

It is not that controversial to say that ASP.NET WebForms was designed in a manner to make web development "safe" for programmers from the world of Windows development, with events tied to controls where processing happens on the server. Unfortunately, this attempt at abstraction is a great example of what Joel Spolsky calls a "leaky abstraction" (http://www.joelonsoftware.com/articles/LeakyAbstractions.html). That is, despite how hard we try to hide that development is being done over the Web, the latency between, say, a user's click on the client side, and the response handled on the server side, means that most developers end up worrying about concerns related to the detail that the framework tries to abstract away.

Put another way, we don't have to do much coding in ASP.NET WebForms before Requests, Responses, and plumbing that makes web programming actually work rear their heads. There is little you can do in ASP.NET WebForms without having to realize that the application is on the Web. The abstraction of a window with click events breaks down very quickly!

ASP.NET MVC grew from a premise that developers are better off admitting that web apps are best thought of as the HTTP-based services they actually are, rather than as an abstraction of a programming model designed for a very different set of assumptions. It does not hurt that the framework is designed with testability in mind. However, this moving from a stateful model to a service-based model has other important ramifications to the F# programmer.

As is covered in Chapter 23, F# is a great language for service development. ASP.NET MVC is merely a more complex type of service that, rather than serving up JSON or XML, serves up HTML based on a HTTP request. In this chapter, we demonstrate writing a completely stateless application using F# and the ASP.NET MVC Framework.

FORECAST'R — THE WORLD'S SIMPLEST WEATHER FORECAST SITE

To demonstrate this, this chapter builds on the weather example started with Chapter 20 to build out a website that implements the following user story:

As a human, to quickly determine whether I should wear a light jacket, a parka, a raincoat, a bikini, or in the case of a forecasted Zombie takeover, body armor. I want to know what weather to expect.

To build this out, the following steps apply:

  • Build out a domain that models a weather forecast.

  • Create a repository that can populate the domain.

  • Build a controller that provides a means for an outside service to access the domain.

  • Add an ASP.NET MVC website that leverages the controller and makes the system usable by humans.

To get started, I encourage you to become familiar with the project setup steps described by Tomas Petricek on his blog: http://tomasp.net/blog/fsharp-mvc-web.aspx. This setup explains how to set up an F#/C# hybrid project that uses C# for views but F# for everything else.

Modeling the Domain

If we are going to have a weather forecasting site, it will probably need to have a model that covers different types of possible weather. Such a model might start as follows:

namespace FSharpBook.Models
type SkyType =
   | Sunny
   | Overcast
   | PartlyCloudy
   | Snow
   | Rain
   | Hurricane
   | Zombies                            
Modeling the Domain

SkyType is a discriminated union that represents different types of weather that are possible. Others may be added, but this is a good initial list that allows a developer to know whether a parka is needed. Others could be added (that is, PartlySunny, Showers, and so on), but the current set should be enough for a really simple site.

Of course, it is not enough just to give an indication of temperature, knowing just because it is sunny, it does not mean that a coat isn't needed. Without a temperature, it could be sunny, but a bone chilling −30 degrees outside! It would also be useful to know the location of our forecast, in case the user entered Paris, but got Paris, Texas instead of Paris, France. The following is added to the same file, ForecastModel.fs:

type Forecast = { Location:string; AverageTemperature:double; Skies:SkyType }

To implement a simple forecast model as asked for in the user story, a simple record type with a Location, AverageTemperature, and Skies — will determine whether it will be wet and how cold or hot it will be.

Of course, something is needed that will provide a means to get a forecast. For that, a repository should be created from which forecast information can be obtained.

It isn't just what the model does, but it is also what the model does not do. It is not doing any work to retrieve actual forecast data from outside sources — that is a concern for the repository. Nor does it care about how anything is rendered — that is a concern for the view — not the model. The only things that belong in the model are considerations about how it is going to internally represent weather, and any behavior that might be added. For example, behavior can be added to Forecast that allows it to return something that specifies what kind of attire should be worn. Assume the following choices of attire the user might be interested in wearing:

type Attire =
  | Bikini
  | Normal
  | LightJacket
| Coat
  | Raincoat
  | Parka
  | BodyArmor                                    
Modeling the Domain

The definition for Attire is a discriminated union, much like the definition for SkyType. Given the above attire, it would be useful to add behavior to specify what kind of attire should be worn in different weather conditions. Such a specification may look like this:

type Forecast with
  member forecast.ToAttire() =
    match forecast.AverageTemperature,forecast.Skies with
      | temp,sky when temp > 75.0 && sky = Sunny || sky = PartlyCloudy -> Bikini
      | temp,sky when temp > 65.0 && sky = Sunny || sky = PartlyCloudy -> Normal
      | temp,sky when
          temp > 45.0 && sky = Sunny || sky = PartlyCloudy -> LightJacket
      | _,Rain -> Raincoat
      | temp when temp < 20.0 -> Parka
      | _,Hurricane -> Coat
      | _,Snow -> Parka
      | _,Zombies -> BodyArmor
      | _ -> Coat //when in doubt, wear a coat...
                                                              
Modeling the Domain

A conversion from a Forecast to Attire is based on mapping the appropriate AverageTemperature and Skies combinations to the appropriate Attire. So in a case where there are temperatures over 75 and at least PartlyCloudy skies, the recommendation will be Bikini. There are various other recommendations, including BodyArmor in the event of Zombies.

Note the reopening the Forecast type later on in the model definition, in this case, to add new behavior. This is not that uncommon in F#, as frequently, a method needs to be added that uses a type that was not known earlier because of sequential evaluation. (Remember, types can't be used prior to being defined.)

For a simple website that models a weather forecast and attire recommendations, this is certainly a good model to start with. With this in place, the next phase is to move on to implementation of a repository from where a forecast can be retrieved.

Creating a Repository

A repository is generally considered a source for information that can be used to populate a domain. In many applications, a repository can take the form of a relational database, such as Postgres, MS SQL Server, or Oracle. CouchDB, MongoDB, db4o, among others, are also great candidates for repositories.

Whatever technology is chosen, the domain objects should never have a need to be aware of it. That is, they should be persistence ignoranant. This is especially important given the increasing rate of innovation in the world of storage and the increasing likelihood you may want to swap out, say, a SQL Server repository at some point for one based on a technology more oriented toward the cloud in the future.

In this case, however, there is no need for traditional data storage. The simple "Forecast'R" site merely uses various Yahoo APIs to gather weather and location information. The signature for the repository method is as follows:

namespace FSharpBook.Repositories
open System
open System.Xml.Linq
open FSharpBook.Models

type YahooForecastRepository() =
  static member GetForecastByLocation locationName =
                                                           
Creating a Repository

Retrieval of "Where On Earth ID"s

The goal, of course, is to retrieve a single forecast based on a name of a location. Chapter 20 provided a means to gather information from a weather forecast based on a Yahoo "Where On Earth ID" (WOEID), but only a specific number for that ID as provided to keep things simple. However, this application is going to need to get such an ID, based on a textual location query, to be of much use. Thus, this first line in the repository routine:

let yahooWhereOnEarthIds
  = YahooForecastRepository.GetWhereOnEarthIdsByLocation locationName

For more about Yahoo "Where On Earth ID"s, please visit developer.yahoo.com/geo/geoplanet/.

This routine will be expected to return one or more "Where On Earth ID"s. In this case, it is implemented as a static member of the repository, because the repository has no state of its own. The implementation is as follows:

static member GetWhereOnEarthIdsByLocation locationName =
//visit http://developer.yahoo.com/geo/geoplanet/ to get your
//  own appId
  let appId = ""
  let yahooUrlLocationLookup
    = sprintf "http://where.yahooapis.com/v1/places.q('%s')?appid=%s"
        locationName appId
  let locationsDoc = yahooUrlLocationLookup |> XDocument.Load
  let yahooWhereOnEarthIds =
    locationsDoc.Descendants()
    |> Seq.filter (fun e -> e.Name.LocalName = "woeid" )
    |> Seq.map (fun e -> e.Value |> Int32.Parse )
  yahooWhereOnEarthIds
                                                               
Retrieval of "Where On Earth ID"s

The Yahoo Geoplanet API, as of time of writing, works by passing a simple HTTP GET to a specific URL. For example, if one wanted to find a WOEID for New Jersey, issue an HTTP GET request to the following:

http://where.yahooapis.com/v1/places.q('New Jersey')?appid=YourAppID

In this example, replace YourAppID with your ID provisioned by the Yahoo Geoplanet service.

What is returned is XML content that has a lot of information about New Jersey. The requirement, however, is to ignore most of this information and simply return a set of integers related to the "Where On Earth ID"s in the response. The file itself contains a set of elements with the local name woeid, each of which (there may be more than one) contains the integer of interest. After filtering by the LocalName, and then parsing into an Int32, a sequence of "Where On Earth ID"s related to the location is returned.

From "Where On Earth ID"s to Weather Content

The next four lines of the main repository routine allow retrieval of the specific document that relates to the first "Where On Earth ID" returned from GetWhereOnEarthIdsByLocation:

static member GetForecastByLocation locationName =
  let yahooWhereOnEarthIds
    = YahooForecastRepository.GetWhereOnEarthIdsByLocation locationName
  let getForecastDoc id =
    let yahooWeatherRSSUrl =
      sprintf "http://weather.yahooapis.com/forecastrss?w=%d" id
    yahooWeatherRSSUrl |> XDocument.Load
  let weatherDoc = yahooWhereOnEarthIds |> Seq.head |> getForecastDoc

                                                        
From "Where On Earth ID"s to Weather Content

The first step is to implement a function scoped to GetForecastByLocation called getForecastDoc. It takes an id, expected to be an integer that refers to a "Where On Earth ID". The next step is to compose a URL by combining the public Yahoo Weather forecast RSS URL template with the "Where On Earth ID". Next, pass the composed URL to XDocument.Load, which provides the Yahoo Weather content related to the given "Where On Earth ID".

The last line is where the function is called that was defined on the previous three lines. Starting with a sequence of "Where On Earth ID"s, the top one is taken (making an executive decision that the first result is the one the user most likely wanted), and it is passed to the getForecastDoc function, with the result being an XDocument with the weather information that can be used to populate a forecast domain.

Digging Out the Content

The next set of lines of the routine use skills developed in Chapter 20 to dig into the content and find information about the weather:

let firstLocationElement (elems:seq<XElement>) =
  elems |> Seq.filter ( fun(e) -> e.Name.LocalName = "location" ) |> Seq.head
let locationNameFromElement (elem:XElement) =
  XmlHelpers.getAttr elem "city"
    + ", " + XmlHelpers.getAttr elem "region"
    + ", " + XmlHelpers.getAttr elem "country"
let currentConditions (elems:seq<XElement>) =
  let findFirstConditionElement (elems:seq<XElement>) =
    elems |>
    Seq.filter (fun(e) -> e.Name.LocalName = "condition") |> Seq.head
  let forecastText
    = XmlHelpers.getAttr ( elems |> findFirstConditionElement) "text"
  forecastText |> YahooForecastRepository.ForecastTextToSkyType
let averageTemperatures (elems:seq<XElement>) =
  elems
    |> Seq.map (fun(e) -> e.Attributes())
    |> Seq.concat
    |> Seq.filter (
       fun(a) -> a.Name.LocalName = "low" || a.Name.LocalName = "high")
    |> Seq.map (fun(a) -> a.Value |> Double.Parse)
        |> Seq.average
                                                         
Digging Out the Content

To populate the domain, the location name is needed that the search resolved to, the current conditions specified, and the average temperature. The preceding lines use Linq to XML to gather the required information.

The only real interesting thing being done here, from a domain standpoint, is that the raw text provided by the API is converted into the domain language for weather. This conversion goes through YahooForecastRepository.ForecastTextToSkyType:

static member private ForecastTextToSkyType (text:string) =
  match text with
  | t when t.Contains "Partly Cloudy" -> SkyType.PartlyCloudy
  | t when t.Contains "Rain" -> SkyType.Rain
  | t when t.Contains "Snow" -> SkyType.Snow
  | t when t.Contains "Hurricane"
        || t.Contains "Tropical Storm" -> SkyType.Hurricane
  | t when t.Contains "Overcast" -> SkyType.Overcast
  | _ -> SkyType.Zombies
                                                          
Digging Out the Content

This allows for conversion of an arbitrary string into a given SkyType that ultimately becomes the basis for the Attire recommendation. This example starts with a small number of mappings that provide a good starting point. Inclusion of this in the repository is a choice made here because this particular mapping is likely specific to this repository. Other repositories (say, some foreign language weather service), may have different terms that represent different SkyType values.

Returning the Weather Result

Of course, a great deal of setup and parsing routines have already been done, setting up the conditions needed for the following:

static member GetForecastByLocation locationName =
  let yahooWhereOnEarthIds =
YahooForecastRepository.GetWhereOnEarthIdsByLocation locationName
    let getForecastDoc id =
      let yahooWeatherRSSUrl = sprintf
        "http://weather.yahooapis.com/forecastrss?w=%d" id
      yahooWeatherRSSUrl |> XDocument.Load
    let weatherDoc = yahooWhereOnEarthIds |> Seq.head |> getForecastDoc
    let firstLocationElement (elems:seq<XElement>) =
      elems |> Seq.filter ( fun(e) -> e.Name.LocalName = "location" ) |> Seq.head
    let locationNameFromElement (elem:XElement) =
      XmlHelpers.getAttr elem "city"
        + ", " + XmlHelpers.getAttr elem "region"
        + ", " + XmlHelpers.getAttr elem "country"
    let currentConditions (elems:seq<XElement>) =
      let findFirstConditionElement (elems:seq<XElement>) =
        elems |>
        Seq.filter (fun(e) -> e.Name.LocalName = "condition") |> Seq.head
      let forecastText =
        XmlHelpers.getAttr ( elems |> findFirstConditionElement) "text"
      forecastText |> YahooForecastRepository.ForecastTextToSkyType
    let averageTemperatures (elems:seq<XElement>) =
      elems
        |> Seq.collect (fun(e) -> e.Attributes())
        |> Seq.filter
        (fun(a) -> a.Name.LocalName = "low" || a.Name.LocalName = "high")
        |> Seq.map (fun(a) -> a.Value |> Double.Parse)
        |> Seq.average
    let convertDocToForecastModel (doc:XDocument) =
      {
        Location = doc.Descendants()
          |> firstLocationElement
          |> locationNameFromElement;
        AverageTemperature = doc.Descendants() |> averageTemperatures;
        Skies = doc.Descendants() |> currentConditions
      }
    weatherDoc |> convertDocToForecastModel
                                                            
Returning the Weather Result

These last lines provide a function for conversion of an XDocument from the Yahoo Weather API into a domain model for Weather. The function convertDocToForecastModel is a composition of other functions that have been defined. The last line simply passes the actual document to the function, so the actual result can be returned.

It is important to note that this repository is fairly complex compared to many repository implementations we may write. More typical repositories might be based on more traditional database code, as is demonstrated in Chapter 19.

Creating the Controller

When a domain is set up and a repository has been written that is capable of populating a domain, the next step is to implement a means to connect the domain to something more interesting. In the ASP.NET MVC framework that takes the form of a controller. The controller is, to put it simply, a means for a domain to be exposed to the outside world, typically a View somewhere.

A good way to think about a controller is to think about how a user somewhere may interact, logically, with the application. A simple site for viewing a forecast will definitely need an Index page, which likely has instructions on it, and perhaps a box where someone can enter a query. Certainly something will be needed to handle the query. It would probably also be a wise idea to have a page that shows a result somewhere — one that is preferably a result from the aforementioned query.

Setting Up the Controller

Start a controller by adding a file, perhaps controllers.fs, and adding the following code:

namespace FSharpBook.Controllers
open System.Web.Mvc
open FSharpBook.Models
open FSharpBook.Repositories
open System.Web.Routing

[<HandleError>]
type ForecastController() =
  inherit Controller()
                                       
Setting Up the Controller

By convention, all controller classes in ASP.NET MVC end in the word Controller. The part of the name before Controller, Forecast in this case, will become the name of the controller that is used by the View logic when mapping routes to controllers. The controller should be aware of the domain (in FSharpBook.Models), as well as the repositories (in FSharpBook.Repositories). The initial implementation will use concrete versions of those classes, though it is certainly possible (and ultimately desirable!) to use dependency injection to map controllers to a domain and a repository.

Of course, it is worth noting that the class derives from Controller, which provides it with a good deal of default controller functionality out of the box that will be used as this sample site is built.

The Index Action

The index page, frankly, will not do much. It is expected to probably have some text on it, and perhaps a form that might take in some information. However, neither the controller nor the actions defined in a controller care much about that form. What the form actually contains is a responsibility of the view. It is known that the index page displays the same thing, no matter who or what is using it. The index is defined as follows within the forecastController:

member forecastController.Index() =
  forecastController.View()

The preceding code dictates that when the Index is asked for, provide a default view for that index. Note that the member must always have the () attached to indicate to the framework that will call the routine that there are zero parameters — as while it is common to leave off the parens in pure F# programs, the framework expects that the signature be a proper zero parameter member and will not map properly without the parens.

The DoQuery Action

Of course, it would be useful to actually have some code that does something. The Submit action, the second written in this example, will perform that role.

When the form is submitted, be it by the Index page, or in theory, any other page that has a form that posts to the Submit action of the Forecast controller, something useful needs to be done with it. There are a couple of options. One naive approach many use is to simply do the work right on the submit action, returning a View that renders the forecast. Although this approach is simple, it causes the great annoyance of the type of message shown in Figure 21-1.

FIGURE 21-1

Figure 21.1. FIGURE 21-1

To get around this, the form processor is going to implement what is often called the PRG Pattern. PRG stands for Post, Redirect, and Get. This has a couple of advantages — one of which is that it gets away from having the browser complain that it is going to need to re-post the form. Another though, and probably more compelling than that, is that it allows separation of form processing from the view rendering that is the result of that processing. The code to implement a PRG pattern is actually quite simple:

member forecastController.Submit(locationQuery:string) =
  let rvd = new RouteValueDictionary()
  rvd.Add("locationQuery",locationQuery)
  forecastController.RedirectToAction("for","forecast",rvd)

A forecast controller always takes a locationQuery as a parameter. This may be a form field from some form that posted to this page, but how it looked on such a form does not really matter to the controller — the controller just needs to know that by the time this routine is called, something mapped it in properly. What is needed is to create a redirect to the controller and action that is actually going to render the result.

One technique to do that is to create a RouteValueDictionary object that has the parameter. The first two lines of the method that create the dictionary and add the item accomplish that goal. The third line is a call to RedirectToAction, a method on the Controller base class that knows how to redirect a new GET request based on an action, a controller name, and optionally, a RouteValueDictionary that holds the parameters.

The For Action

The last action is named For, as it helps when visualizing a URL that might read /Forecast/For?someQuery. The job of For is to actually render the forecast the user is interested in. As it turns out, the method to do so is actually pretty tame:

member forecastController.For(locationQuery:string) =
  locationQuery
    |> YahooForecastRepository.GetForecastByLocation
    |> forecastController.View

Again, it is expected that locationQuery will be passed as a parameter from the view. The repository knows how to retrieve a single forecast based on the query, so the simplest thing to do is to pipe the query into the repository, providing the forecast. That result is then piped into a View, which presumably is structured in such a way as to render the domain object.

Of course, there are a couple schools of thought about rendering domain objects directly on views. It works fine for a simple case like this one, where the object is not terribly complex. However, sometimes, one may want a less complex object than the entire domain object to be sent to a view. Presentation objects, ViewModels, or other abstractions come to the rescue in such situations. However, even in the presence of one of those, it belongs squarely in the domain model part of the MVC framework, not in the Controller, lest you end up inadvertently ending up with a "fat controller" that is typically a code smell that happens when one mixes domain and controller concerns.

Creating Some View Helpers

It should be noted that the view is not quite ready for implementation. There are a couple of things that need to be done to have the domain objects render nicely in the views. The Attire and SkyType objects, although definitely in a nice form useful for the domain logic, do not have a reliable means to convert to strings. Of course, we could interrogate their type and push out a string based on that, but then there would still need to be a means to convert from something like "PartlyCloudy" to, say, "Partly Cloudy", or perhaps if the website needed to render in Spanish, "Parcialmente Nublado".

It would be bad practice to put such view-specific concerns in the same place as the main controller or model, so to keep the concerns separate, put these in a separate area of the project called ViewHelpers. The first new file will help us render various SkyType values:

module SkyTypeRenderer
open FSharpBook.Models

let ToWeatherString sky =
 match sky with
   | Sunny -> "Sunny"
   | Overcast -> "Overcast"
   | PartlyCloudy -> "Partly Cloudy"
   | Snow -> "Snow"
   | Rain -> "Rain"
| Hurricane -> "Hurricane"
   | Zombies -> "Oh NO! ZOMBIES!!!"
let ToImageFileName sky =
 match sky with
   | PartlyCloudy -> "PartlyCloudy"
   | Zombies -> "Zombies"
   | _ -> ToWeatherString sky
                                                
Creating Some View Helpers

This first file allows us to do two view-specific things. It creates a string for each state of sky (to be rendered in a specific view), and it maps sky types to filenames, which again, are known when dealing with a specific view.

The second file will help map various Attire definitions to strings:

module AttireRenderer
open FSharpBook.Models

let ToAttireString attire =
  match attire with
    | Bikini -> "Bikini"
    | Normal -> "Normal"
    | LightJacket -> "Light Jacket"
    | Coat -> "Coat"
    | Raincoat -> "Raincoat"
    | Parka -> "Parka"
    | BodyArmor -> "Body Armor"
                                                      
Creating Some View Helpers

Of course, what is being done here with these renderers, in a production system, would be best done with locale-specific resource files, so that when a system is internationalized, such internationalization is more easily done by swapping out the appropriate resource.

Creating the View

If everything has been done right, there should be almost no reason to have code, other than perhaps a few very trivial binding helpers, in the view. The Holy Grail in this kind of system is a view that lacks any code at all. That is, the view should consist of markup that is the province of the user interface designer, and anything that such a designer would have a hard time understanding should go in some other kind of component.

The example site has two views. It has the Index view that is rendered when the user goes to the default page, and the results view (For.aspx, in this case) that shows the result. The Index view (Index.aspx) is very simple:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="titleContent" ContentPlaceHolderID="TitleContent" runat="server">
    Forecastr - The simplest weather forecast system in the Universe!
</asp:Content>
<asp:Content ID="mainContent" ContentPlaceHolderID="MainContent" runat="server">
<div id="queryText">
<% using (Html.BeginForm("DoQuery","forecast", FormMethod.Post)) {%>
    I want to know the weather forecast for:
    <%= Html.TextBox("LocationQuery") %>
    <input type="submit" value="Get Forecast" />
<% } %>
</div>

</asp:Content>
                                                 
Creating the View

It is notable that what little code is here is written in C#, not F#. Although it may be possible to get markup to work in F#, doing so requires a lot of work with web.config files and certainly does not work easily out-of-the-box as of the time of writing. Given the intent is to minimize code in the view anyway, in this case, a decision is made to keep view-only code in C# — but do as little as possible.

The code that is here, frankly, does very little. Simple helpers are being used from the framework to generate some html code. Html.BeginForm is a framework helper function for spitting out form elements; Html.TextBox does the same for Input elements that represent text boxes. The only thing that really needs to be done here is to specify a controller that should be posted to, as well as an intended action. We could just as easily write:

<form action="/forecast/DoQuery" method="post">

Of course, the benefit received in using the helper is that it can provide some help in making sure the output is correct, because the preceding code might not be correct if we were in a situation that involved writing a site that ran from a virtual directory. That said, there may be cases where a designer will want more control over the output and will opt for writing the raw html. Given that the only responsibility of the html helper is to spit out appropriately formatted html, having an html designer hand-code it would not break the site.

The code for the Forecast view is a slightly bit more complex, but not horribly so:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage" %>
<%@ Import Namespace="FSharpBook" %>
<%@ Import Namespace="FSharpBook.Models" %>

<asp:Content ID="titleContent" ContentPlaceHolderID="TitleContent" runat="server">
    <%
        var forecastModel = (Forecast) ViewData.Model;
    %>
    Forecast for <%= forecastModel.Location %>
</asp:Content>

<asp:Content ID="mainContent" ContentPlaceHolderID="MainContent" runat="server">

    <%
        var forecastModel = (Forecast) ViewData.Model;
    %>
    <table><tr>
    <td>
<div id="weatherIcon">
        <img src="../../Content/<%=
SkyTypeRenderer.ToImageFileName(forecastModel.Skies) %>.gif" alt="<%=
SkyTypeRenderer.ToWeatherString(forecastModel.Skies) %> Graphic" />
    </div>
    <div id="weatherText">
        <%= SkyTypeRenderer.ToWeatherString(forecastModel.Skies)%>
    </div>
    </td>
    <td>
    <div id="weatherIntroText">We are forecasting that the weather in <%=
forecastModel.Location %> will be:</div>
    <div id="averageTemperatureDisplay">
        <%= forecastModel.AverageTemperature %> degrees with <%=
SkyTypeRenderer.ToWeatherString(forecastModel.Skies)%>
    </div>
    Consider wearing <%= AttireRenderer.ToAttireString(forecastModel.ToAttire()) %>
    </td>
    </tr></table>
    <%= Html.ActionLink("Back to Index","Index","Forecast") %>
</asp:Content>
                                                      
Creating the View

Again, this is being done in C#, but doing so using the minimum of executable code. Starting with the title part of the page, it starts off with a user friendly title so that when the end user has multiple browser windows open, they will know which one is the Forecast page. ViewData.Model carries an object that represents the thing that the page should bind to. In this case, to make this work a strongly typed reference to the model is useful:

<%
     var forecastModel = (Forecast) ViewData.Model;
%>

This allows the following snippet that sets the title text to be written:

Forecast for <%= forecastModel.Location %>

The body of the page is a little more work. Note that each page content area, delimited by <asp:Content> tags, has independent scope, so it is necessary to put in a strongly typed declaration for forecastModel again:

<%
      var forecastModel = (Forecast) ViewData.Model;
%>

The first bit of code that is encountered is for the icon used to display the weather. The view helper has a routine to provide the name of a file. The page is going to leverage that to provide the html that will be used to show the image:

<div id="weatherIcon">
   <img src="../../Content/<%=
SkyTypeRenderer.ToImageFileName(forecastModel.Skies) %>.gif" alt="<%=
SkyTypeRenderer.ToWeatherString(forecastModel.Skies) %> Graphic" />
    </div>

In this case, the code is appending the string for the image file to the Content path and then adding .gif at the end. Like all good web citizens, the site provides alt text as well that leverages the ToWeatherString method that was written in the view helper so that the site has a text representation of the image as well.

The same code will be used to generate alt text for the weather headline:

<div id="weatherText">
   <%= SkyTypeRenderer.ToWeatherString(forecastModel.Skies)%>
</div>

Again, the page is using the SkyTypeRenderer to translate from a logical type of weather represented by forecastModel.Skies to the actual string the page should use for rendering.

The next couple sections follow the same pattern:

<div id="weatherIntroText">We are forecasting that the weather in <%=
forecastModel.Location %> will be:</div>
    <div id="averageTemperatureDisplay">
        <%= forecastModel.AverageTemperature %> degrees with <%=
SkyTypeRenderer.ToWeatherString(forecastModel.Skies)%>
    </div>
                                                             
Creating the View

The last section is where a clothing recommendation is made. Again, the logic is not terribly complex; just render the model:

Consider wearing <%= AttireRenderer.ToAttireString(forecastModel.ToAttire()) %>

If the view is much more complex than this, say, rendering a simple domain model, it might be advisable to write a ViewModel — and some classes to convert from the more complex domain to a ViewModel so that the chance of that complex logic getting added to the view is minimized. Code that goes in views tends to not be reused and tends to allow concerns that are not specific to a particular view to leak into views, leading to duplication and difficult maintenance.

SUMMARY

In this chapter, we learned how to leverage F# in our ASP.NET MVC applications — and in so doing, use F# and the fact that F# discourages mutation to our advantage in building a simple yet robust website. One of the striking things you will notice in our implementation of a simple ASP.NET MVC site is that there are zero cases where we change the value of a variable once assigned. A reality of web development is that state across multiple requests, although sometimes useful, is not an inherent part of the medium, like it is on an ordinary WinForm or WPF application. If you have little or no state, each request action type becomes like a separate program with a much more limited scope. Although this does not make bugs go away, it does provide a layer of isolation between the details of various requests, making the code for any given action easier to understand, manage, and test.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset