Drawing lines with Rx

In part 1 and 2 of the series Reactive Extensions in theory and practice we covered some of the theory of Rx and introduced a nice visualization method with marble diagrams.

In this post we will have a closer look at the operators Select, Zip and Skip and review a sample application where those operators are extremely useful.

The application is a very simple WPF application where you can draw lines in a window. For each line the distance will be displayed. In fact the code for this program is so simple and declarative that it really shows the shining beauty and elegancy of Rx.

alt text

The source code of the app can be downloaded here.

Operators

Let’s first introduce the operators that are involved in this example.

Select

Select is a transforming operator that projects the elements of an observable sequence into a new form by applying the function f to each element.

Marble diagram - Select

Skip

Skip is a filtering operator that bypasses the first n elements of a sequence. The result is a sequence that emits all elements of the source sequence except the first n elements.

Marble diagram - Skip

Zip

Zip is a combining operator that merges two sequences by applying a function f to each pair of elements. Pairs are created by combining each element of the first sequence with the element of the second sequence that has the same index as the first element.

Marble diagram - Zip

Operators in practice

To see the operators in action we will review this sample application that lets us draw lines on the UI. For each line the length is displayed. The length is the distance between two positions on the UI represented by two Point instances. In this case the coordinates of the points are determined relatively to the canvas control of the main window.

Let’s go through the steps that are necessary to implement this.

First let’s take a look at the simple class Line which is the main type used in the application:

public class Line
{
    public Point PointA { get; private set; }
    public Point PointB { get; private set; }

    public double Length
    {
        get
        {
            var d = Point.Subtract(PointA, PointB);
            return Math.Sqrt(Math.Abs(d.X * d.X - d.Y * d.Y));
        }
    }

    private Line(Point a, Point b) { PointA = a; PointB = b; }

    public static Line Create(Point a, Point b)
    {
        return new Line(a, b);
    }
}

The implementation is straightforward. The factory method Create takes two points and returns a new Line instance. It is just for convenience because it simplifies the syntax when used instead of the constructor with Rx operators. Length calculates and returns the distance between the two points. For the sake of simplicity the value of Length is not stored locally which would be an optimization.

Now we have to convert the mouse button click events into an observable stream. This can be done with the Observable.FromEventPattern<TEventArgs> function. As the first parameter we have to provide the object instance that exposes the mouse click event which is the MainWindow instance itself. So we can pass in this here. The second parameter is the name of the event that we are interested in which is "MouseUp". The result is an observable sequence of EventPattern<MouseButtonEventArgs>:

Observable.FromEventPattern<MouseButtonEventArgs>(this, "MouseUp")

Next we have to convert each EventPattern into a Position of type Point. We could do this by just using a lambda expression. But instead we define the little nested helper function getPosition. The benefit of the helper function is that it makes the code more compact later when the function is passed to the Select operator. The position is determined relatively to the Canvas UI control named _box.

Func<EventPattern<MouseButtonEventArgs>, Point> getPosition = 
    args => args.EventArgs.GetPosition(_box);

Finally we need a function that takes a line and updates the UI by drawing the line and displaying its length. Therefore we define a private method UpdateUi. The details here are WPF specific and not really relevant to us right now.

Composing the operators

Now we have defined all necessary functions. The only thing left to do is to transform and combine the observable sequence of click events by composing and applying the Rx operators Select, Skip and Zip.

The idea is to create lines by taking each point of the sequence and combine it with its predecessor.

The solution is simple and elegant. After we transformed the click stream to positions with Select(getPosition) we shift the sequence by one with Skip(1). Then we merge the shifted sequence with the original sequence with Zip and pass in the factory method to create lines. That’s it!

There is no need to keep the state e.g. the last clicked position in a local or instance variable.

Here is marble diagram that visualizes how this works:

Marble diagram - Draw lines

Connecting the observer

Finally we have to connect the observer (which is the UI in this case) with the observable by calling the extension method Subscribe and pass in the UpdateUi function. Here is the code that is responsible for composing all the functions to create the program:

var points = Observable
    .FromEventPattern<MouseButtonEventArgs>(this, "MouseUp")
    .Select(getPosition);

points
    .Zip(points.Skip(1), Line.Create)
    .Subscribe(UpdateUi);

Summary

We have seen how a program can be composed by using observable sequences and applying Rx operators to them. The source code of this program is very concise and declarative. Also there is no state involved (except for the UI off course) that we have to be cautious of.

Using Reactive Extensions in event-based applications leads to code that is simple and elegant and therefore easy to understand, maintain and extend.

Resources

Our Hackathon from the ChatGPT

Our team meets at regular intervals to work on projects together, to programme or to hold a “hackathon”. It is important to us that every

How to write cleaner parsing code

When tasked to extract information from a string, most seasoned developers – especially the ones with some kind of Linux background – will resort to