Step 2: Implement Voting for an Option
Applies to: Functional Programming
Authors: Tomas Petricek and Jon Skeet
Get this book in Print, PDF, ePub and Kindle at manning.com. Use code “MSDN37b” to save 37%.
Summary: This step adds support for voting. It adds a new action to the F# controller and also explains how to modify the mapping of the routes so that the voting link can use a nice URL.
This topic contains the following sections.
- Implementing Voting
- Routing URL Requests
- Summary
- Additional Resources
- See Also
This article is associated with Real World Functional Programming: With Examples in F# and C# by Tomas Petricek with Jon Skeet from Manning Publications (ISBN 9781933988924, copyright Manning Publications 2009, all rights reserved). No part of these chapters may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.
Implementing Voting
The previous step demonstrated how to create an ASP.NET MVC web application that loads information about the poll from the database and displays the current results to the user. The application also generated links to allow the users to vote. The links have neat URLs, such as /Vote/3, where 3 is the option's database ID. This article shows how to handle these requests to update the database.
Adding a Stored Procedure
The code written in this step uses the same technologies as the examples from the previous section. Updating the database is implemented as a stored procedure that will be called from F# code later. The stored procedure takes a parameter representing the option ID and it increments the number of votes:
CREATE PROCEDURE dbo.Vote ( @id int )
AS
UPDATE [PollOptions] SET [Votes] = [Votes] + 1
WHERE [ID] = @id
The snippet shows an SQL script that creates a stored procedure if it doesn't already exist in the database. If it exists, it can be updated using the ALTER PROCEDURE command. The body of the procedure is a simple UPDATE statement that adds 1 to the Votes column of all rows with the specified ID. Because ID is the table's primary key, the update always affects one row at the most.
Calling a Procedure from a Model
The previous step of the tutorial demonstrated how to call a parameterless procedure that returns a collection of rows as a result. The procedure in the previous section is different. It takes a single input argument and doesn't return any results. When using ADO.NET directly, this would require several changes to the source code, but, using the F# wrapper, this is surprisingly simple.
Note
As in the previous step, the next listing uses the DynamicDatabase helper to call the procedure using the dynamic operator (?). For more information see How to: Dynamically Invoke a Stored Procedure or download the complete source code below.
The code for connecting to the database should be a part of the model so it can be placed into the Model.fs file (in the F# project FSharpWeb.Core). To compare both of the stored procedure calls side by side, the following listing includes the LoadPoll function from the previous step as well as the new Vote function:
module Model =
let connectionString = "..."
let LoadPoll() =
[ let db = new DynamicDatabase(connectionString)
for row in db.Query?GetOptions() do
yield { ID = row?ID; Votes = row?Votes;
Title = row?Title; Percentage = row?Percentage } ]
let Vote(id:int) =
let db = new DynamicDatabase(connectionString)
db.NonQuery?Vote(id)
The Vote function doesn't return a result, so the body is not wrapped in a list comprehension. Just like LoadPoll, it starts by creating an instance of the DynamicDatabase helper and then uses it to run the query.
When running a SQL command that doesn't return results, the call should be made via the NonQuery property. This instructs the wrapper to use the ExecuteNonQuery method of the underlying ADO.NET command object. The dynamic operator provided by NonQuery always returns unit, so there is no danger of incorrectly casting the result. By contrast, the Query property used in the LoadPoll function exposes a different implementation of the dynamic operator, which always returns a sequence of rows.
Both of the operators handle arguments in the same way. When calling a stored procedure, it is possible to specify arguments. This actually just passes an additional tuple value to the dynamic operator and the operator uses F# reflection to extract individual values. The arguments are not checked at compile time, so the call may fail at runtime when the number of arguments is wrong or when the types do not match.
The Vote function is a part of the ASP.NET MVC model. It is called by a controller component, which is implemented in the next section.
Implementing a Controller Action
The action that implements voting doesn't have a corresponding user interface. When the user clicks on the link, the page navigates to a new URL and sends a request to the server. The server records the vote and redirects the user back to the main page. This means that there is no view part, but there is still a controller part that handles the request. The following listing shows the Vote member that implements the action. It is added to the Main controller (in Main.fs of the FSharpWeb.Core project) and takes the option ID as an argument:
type MainController() =
inherit Controller()
member x.Index() =
x.ViewData.Model <- Model.LoadPoll()
x.View()
member x.Vote(id:int) =
Model.Vote(id)
x.RedirectToAction("Index")
Both of the actions in the Main controller are extremely simple, so the listing shows them both. One interesting aspect about the Vote method is that it doesn't call the x.View() method to produce a view associated with the action. Instead, it calls RedirectToAction to specify that the response should redirect the user back to the web application's Index action. Both View and RedirectToAction are just helper methods that build a value of ActionResult type, which is returned back to the MVC Framework. By returning a different value, it is possible to specify that the response is a file for download or that the user doesn't have the rights to request the specified resource (for example, to cast a vote).
At this point, all functionality is in place, but the voting wouldn't work yet. The missing piece is that ASP.NET MVC doesn't know that the URL /Vote/3 should be mapped to the Vote action of the main controller. The next section describes how to specify the mapping.
Routing URL Requests
The default URL mapping rule created by the template specifies that the URL of a form /Controller/Action should be mapped to the Action method implemented by a specified Controller. However, it doesn't specify that there may be any additional arguments (such as the ID of a poll option), so this mapping wouldn't work for the Vote action.
To specify a mapping with additional parameters, you first need to define a new record for the route that describes the additional parameter. The following snippet shows the standard Route record as well as the newly defined RouteWithId:
type Route =
{ controller : string; action : string }
type RouteWithId =
{ controller : string; action : string; id : int }
The second record adds a new filed named id. Note that the name corresponds to the name of the parameter in the declaration of the Vote method in the controller. The type of the field is int because that is the expected type of parameter values. Alternatively, optional parameters can be defined by adding a field of type UrlParameter (and using UrlParameter.Optional as the default value).
The following listing shows the Global type (from the Global.fs file in FSharpWeb.Core). The type configures the URL mapping in the RegisterRoutes method:
type Global() =
inherit System.Web.HttpApplication()
static member RegisterRoutes(routes:RouteCollection) =
routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
routes.MapRoute
( "Default", "{action}",
{ Route.controller = "Main";
action = "Index" } ) |> ignore
routes.MapRoute
( "Voting", "Vote/{id}",
{ RouteWithId.controller = "Main";
action = "Vote"; id = -1 } ) |> ignore
member x.Start() =
AreaRegistration.RegisterAllAreas()
Global.RegisterRoutes(RouteTable.Routes)
The snippet registers two different routes. The first route handles the situation when the URL contains just the name of an action (such as Index). The record value used as the last argument specifies the name of the controller (Main) and the default action. It is created using explicit syntax (Route.controller) that specifies which of the two records types should be used. F# cannot automatically infer the type because the two records have fields with identical names. The Default route provides the default value for the action parameter so it matches the URL referring to the root of the web application. As a result, the Index action is used for the application's home page.
The second route covers the case when the accessed URL is Vote/{id}, where {id} stands for some number. As specified by the record value, the path will be mapped to the Main controller's Vote action. The id value will be used as the value of the Vote method's id parameter.
After you add the routing, you should be able to run the application and vote. When the voting link is clicked, the web server redirects the user back to the main page, so the change should be visible almost instantly.
Summary
This step of the tutorial added the implementation of voting to the poll web site. The user interface provides a link to vote for an option (such as /Vote/3). The tutorial demonstrated how to use ASP.NET routing to specify the handler for this nice URL. A handler is an action (implemented as a method) in the main controller of an application. After the action updates the database, it redirects the user back to the poll home page.
The code that updates the database was implemented as a stored procedure. As opposed to the previous example, the procedure took a parameter but didn't return a result. The tutorial used a helper that made it possible to call stored procedures, even with arguments, using the dynamic operator (?).
Additional Resources
This tutorial demonstrates how to create a web-based poll. The structure of the application was created using an online template. The first step discussed the database and the viewing of the data. Calling the stored procedures was done using the dynamic helper. The following articles discuss all of the previous steps:
The online poll application doesn't perform a large number of I/O operations when handling a request, so it can be implemented synchronously. However, many applications call other services and should be implemented without blocking threads. This is discussed in the following tutorial:
To download the code snippets shown in this article, go to https://code.msdn.microsoft.com/Chapter-5-Bulding-Data-ec639934
See Also
This article is based on Real World Functional Programming: With Examples in F# and C#. Book chapters related to the content of this article are:
Book Chapter 7: “Designing data-centric programs” explains how to design applications that are primarily designed to work with or create a data structure. This is very often the design used by ASP.NET MVC models.
Book Chapter 12: “Sequence expressions and alternative workflows” contains detailed information on processing in-memory data (such as lists and seq<'T> values) using higher-order functions and F# sequence expressions.
Book Chapter 13: “Asynchronous and data-driven programming” explains how asynchronous workflows work and uses them to write an interactive script that downloads a large dataset from the Internet.
The following MSDN documents are related to the topic of this article:
ActionResult Class contains more information about possible results returned from an action, such as redirecting or returning a file.
ASP.NET Routing explains how URL routing works in ASP.NET, how to declare parameters, and how to use the MapRoute method.
Previous article: Step 1: Create a Database and Show the Poll Options
Next article: How to: Dynamically Invoke a Stored Procedure