Tutorial: Reactive Programming Using Events

Applies to: Functional Programming

Authors: Tomas Petricek and Jon Skeet

Referenced Image

Get this book in Print, PDF, ePub and Kindle at manning.com. Use code “MSDN37b” to save 37%.

Summary: This article discusses how to create reactive Silverlight applications using F#'s first-class events and higher-order functions for composing events.

This topic contains the following sections.

  • Creating a Color Selector
  • Building the User Interface
  • Implementing User Interaction
  • 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.

Creating a Color Selector

This article demonstrates how to create a user control for selecting a color. The control consists of three sliders where the user can change the values of individual color components (red, green, blue) and a panel that shows the currently selected color. A sample application using the control is shown in Figure 1.

Figure 1. A user control for selecting colors

Referenced Image

A typical imperative way to implement this control is to create three mutable fields to hold the current red, green, and blue components. When a slider is moved, it modifies the corresponding field and updates the color displayed in the preview. This solution is very imperative and it specifies "how" to implement the behavior.

A functional approach is to specify "what" the required behavior is. This can be done by describing the application data flow. The current values of the sliders are combined together into a single value that defines the color. In F#, this idea can be encoded using first-class events (a feature that enables you to use events as values) and functions for working with events. Although the example is quite simple, the approach scales very well and is applicable to fairly complicated scenarios where an application needs to react to user input or completion of background tasks.

Before looking at how to implement the behavior, the following section briefly discusses how to create the user interface of the user control.

Building the User Interface

The discussion in this tutorial assumes that the code is part of a standalone F# Silverlight project. It uses a XAML file to define the user interface and it accesses the controls defined in XAML using the dynamic operator (?). Information about the project structure and the techniques used can be found in the following related articles:

To start, create a new F# project using the "F# Client-Side Application (Silverlight)" template from the Online Templates pane of the New Project dialog.

Defining Controls Using XAML

Once an F# Silverlight project is created, the next step is to add a new XAML file (e.g., Selector.xaml) that defines the user interface. The layout from Figure 1 can be created using two nested StackPanel controls:

<StackPanel Orientation="Horizontal">
    <Canvas Width="60" Height="60" Margin="15" 
            Background="Black" x:Name="ColorBox" />
    <StackPanel Orientation="Vertical" Width="150">
        <Slider Minimum="0" Maximum="255" Margin="0 5 0 5" 
                Height="20" x:Name="SliderRed" />
        <Slider Minimum="0" Maximum="255" Margin="0 5 0 5" 
                Height="20" x:Name="SliderGreen" />
        <Slider Minimum="0" Maximum="255" Margin="0 5 0 5" 
                Height="20" x:Name="SliderBlue" />
    </StackPanel>
</StackPanel>

The first StackPanel control splits the area into two parts. The left part contains a Canvas element that shows the current color, and the right part contains sliders for adjusting the three color components. The range of the sliders corresponds to byte values (from 0 to 255). The default value of all sliders is zero, which matches the initial background color of the Canvas, which is black.

Note that all elements that need to be accessed from the F# code have the x:Name attribute. The next section uses these names and the dynamic operator (?) (defined in How To: Access User Interface Elements Dynamically) to access the controls.

Creating a User Control Class

The next step of creating an F# user control is to add the F# source file that implements the control functionality (e.g., Selector.fs). This section shows the boilerplate code that loads the user interface from the XAML file. The snippet also creates local fields for all named GUI elements (a canvas that displays the current color and the three slider elements):

namespace ColorSelector

open System
open System.Windows 
open System.Windows.Media
open System.Windows.Controls
open FSharp.Silverlight

type Selector() as this =
    inherit UserControl()
    let uri = "/ColorSelector;component/Selector.xaml"
    do Application.LoadComponent(this, Uri(uri, UriKind.Relative))

    let sliderR : Slider = this?SliderRed
    let sliderG : Slider = this?SliderGreen
    let sliderB : Slider = this?SliderBlue
    let previewBox : Canvas = this?ColorBox

    // TODO: Add user interaction handling here

The details of this snippet are discussed in the articles mentioned earlier. In short, the first few lines of the constructor create a Uri object that refers to the XAML file in the resources and construct the user interface using the LoadComponent method.

The subsequent four let declarations use the dynamic operator (?) to get references to the user interface elements that the F# code works with. The dynamic operator enables using member access syntax (with a question mark instead of a dot) to access properties that cannot be statically checked at compile time. The operator implementation that finds a XAML element by name comes from the FSharp.Silverlight namespace and is discussed in a how-to article mentioned earlier.

The snippets that were explored so far are mostly just boilerplate code to create the layout of the control and define a new class for the control. The next section looks at a model showing how the controls interact. The implementation then directly encodes the model.

Implementing User Interaction

The easiest way to understand the desired behavior is to view it as a data flow. When a user changes the slider position, the data (color update) flows to the control that shows the color preview. Figure 2 visualizes the data flow using a diagram.

Figure 2. The data flow of the color selector control

Referenced Image

The diagram contains three sources of updates. The boxes on the left (such as red.Changed) represent events that occur when one of the sliders is moved. Each event occurrence carries some value. In the case of the Changed event, the value is a simple class derived from EventArgs that contains the original and the new values of the slider. The map function is used to transform the EventArgs value into a function value that specifies how the event updates the currently selected color.

Note

F# views .NET events as values of the IEvent<'T> type, where 'T is the type of values generated by the event (such as EventArgs). Additionally, the IEvent<'T> type inherits from the IObservable<'T> interface, which has a similar purpose but is part of the standard .NET libraries.

When working with events, F# developers can use functions from the Observable and Event modules. In general, it is better to use the former because it provides better support for unregistering event handlers. Simple tasks can also be implemented using the Event module, which provides a similar set of functions. More information about these two modules can be found in Chapter 16 of Real World Functional Programming as well as in the links at the end of the article.

The following listing shows F# code that corresponds to the first two columns in Figure 2. When calling Observable.map, currying is used to create an event that carries a function:

let redUpdate = 
    sliderR.ValueChanged 
    |> Observable.map (fun ea (r, g, b) -> byte ea.NewValue, g, b)
let greenUpdate = 
    sliderG.ValueChanged 
    |> Observable.map (fun ea (r, g, b) -> r, byte ea.NewValue, b)
let blueUpdate = 
    sliderB.ValueChanged 
    |> Observable.map (fun ea (r, g, b) -> r, g, byte ea.NewValue)

The Observable.map function takes a function that converts the value carried by the source event to a new value. In the snippet above, the original value is ea. However, the function in the snippet takes another parameter (a triple of RGB values). This is not a mistake. It means that the result of the projection is a function. This becomes more obvious when the function is created explicitly:

let redUpdate = 
    sliderR.ValueChanged 
    |> Observable.map (fun ea -> 
        (fun (r, g, b) -> byte ea.NewValue, g, b))

The snippet is an equivalent of the previous one but it demonstrates better what is going on. When given event arguments of a slider change event (ea), the outer lambda function returns a ColorTriple -> ColorTriple function (where ColorTriple stands for byte * byte * byte). The resulting function specifies how to turn the previous color value into a new color.

The next step in the diagram in Figure 2 was to merge the three events. The result is a single event carrying updates for any of the color components. This can be done using the Observable.merge function:

let allUpdates =
    [ redUpdate; greenUpdate; blueUpdate ]
    |> List.reduce Observable.merge

The Observable.merge function takes just two arguments. Three events could be merged by first merging two of them and then merging the result with the third event. The above snippet uses a more general approach. It calls List.reduce, which takes a function for merging two values and merges an entire (non-empty) list using the function.

The type of the result is IObservable<ColorTriple -> ColorTriple>. As the last step, the update event needs to be turned into an event that yields the current color each time an update is produced. To do that, the event needs to keep some state and apply the incoming updates to that state. This pattern is captured by the Observable.scan function:

let brushes = 
    allUpdates
    |> Observable.scan (fun clr f -> f clr) (0uy, 0uy, 0uy)
    |> Observable.map (fun (r, g, b) -> 
           SolidColorBrush(Color.FromArgb(255uy, r, g, b))) 

The arguments of Observable.scan are an aggregation function and an initial value. In the above example, the initial value is a triple of bytes representing the color black. When the source event occurs, producing some value, Observable.scan calls the aggregation function with the previous state and the value produced by the source event. In the above example, the function gets the previous color and a function to update the color, so it just applies the update function to the color. The function used by the scan is identical to the pipelining operator, so the call could look like this: Observable.scan (|>) (0uy, 0uy, 0uy). The snippet uses a more explicit style for better readability.

The Observable.scan function returns an event that is triggered each time the state changes. The pipeline then calls Observable.map to turn the event-producing RGB triples into an event that generates SolidColorBrush values. As the last step, the program needs to register a handler that uses the generated brush to fill the preview box:

do  brushes |> Observable.add (fun br -> 
        previewBox.Background <- br)

The Observable.add operation takes a function that performs an imperative action for every generated value. In the above case, it modifies the Background property of the previewBox control.

Summary

This article implemented a simple reactive component using F#'s first-class events and functions for declarative event processing. This approach is a great fit for problems that can be modeled using data flow diagrams. In the color selection control, the user modifies values of individual color components. The changes of color components are propagated to a control that shows a preview of the current color.

The example also demonstrated a typical approach for implementing declarative event processing. The starting point is a data flow diagram, which is then directly encoded using functions from the F# Observable module. At the end of the event-processing pipeline, there is usually some final action that updates the user interface. This can be specified using the Observable.add function.

Additional Resources

This article demonstrated working with first-class events using a sample Silverlight application. As already mentioned, the tutorial used a template and helper functions that are discussed in the following articles:

An alternative approach to implementing reactive applications is to build on top of F# asynchronous workflows. The following articles give overviews and comparison of the two approaches and provide more details about the second one:

To download the code snippets shown in this article, go to https://code.msdn.microsoft.com/Chapter-3-Reactive-Client-8a458f7d

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 9: “Turning values into F# object types with members” explains how to use object-oriented features of the F# language. This is an important topic for client-side development because it explains how to mix the object-oriented Silverlight design with the functional programming style.

  • Book Chapter 16: “Developing reactive functional programs” discusses how to write reactive user interfaces using asynchronous workflows and events. It also includes a basic introduction to the Reactive Framework (Rx).

The following MSDN documents are related to the topic of this article:

Previous article: Tutorial: Creating a Silverlight Project in F# Using the Online Templates

Next article: Tutorial: Implementing a Chat Client Using Asynchronous Workflows