Inside F#

Brian's thoughts on F# and .NET

An RSS Dashboard in F#, part six (putting it all together with a WPF GUI)

Posted by Brian on February 18, 2010

Previous posts in this series:

In the previous blog posts, we’ve created all the components we need for the RSS Dashboard app.  Today we just need to assemble all these pieces into an application with a WPF GUI. 

There’s a fair bit of code for today (about 220 lines), but each section is small and understandable on its own, so I’ll walk through it piece by piece.  Recall that the final goal is an app with a UI that looks like

Dashboard

To quote myself from part one:

The window is divided up into a grid where each cell is a different topic/forum.  Within each cell are the six threads with the most recent activity, sorted in order of recency.  Each thread displays its title (long titles are truncated with an ellipsis) and how long ago the thread began.  The threads are hyperlinks, so if I click one it opens the corresponding thread in my web browser.

The app polls the various RSS feeds periodically for new activity.  When new activity is detected, a few things happen:

  • a new entry appears in the listing for the corresponding topic (or, if the thread was already displayed in the list, it gets moved to the top)
  • the thread gets a yellow highlight so that it visually stands out as new
  • (optionally) the title is read aloud over the speakers using the text-to-speech built in to Windows

So let’s get to it!

Easy preliminaries

To start things off, I open a bunch of namespaces I’ll need.

open System
open System.Diagnostics 
open System.Windows.Media
open System.Windows.Media.Imaging
open System.Windows 
open System.Windows.Controls 
open System.Windows.Documents 
open System.ServiceModel.Syndication 

Speech.EventuallySpeak("Starting dashboard")

let NumItemsPerFeed = 6
let FeedUpdateFrequency = TimeSpan.FromMinutes(15.0)

I also announce the start of the program, and define a couple constants used throughout the rest of the app.

Quickly load some icons

I need to get the StackOverflow and hubfs “favicons”.  It turns out that fetching each of these from the web takes on the order of three seconds, so I fetch them in parallel to shorten the application’s startup time. 

let GetIcon (uri:string) =
    let iconDecoder = new IconBitmapDecoder(new Uri(uri), BitmapCreateOptions.None, BitmapCacheOption.Default)
    iconDecoder.Frames.[0]
let [| soIcon; hubfsIcon |] = 
    [| async { return GetIcon "http://sstatic.net/so/favicon.ico" } 
       async { return GetIcon "http://cs.hubfs.net/favicon.ico" } |]
    |> Async.Parallel |> Async.RunSynchronously

That’s a simple use of F# ‘async’ for fork-join parallelism – create an array of async computations and run then in parallel.

Details about the feeds

Now for the feeds I want to appear in the grid.  I have some feeds from StackOverflow as well as the hubfs feed.

It turns out that whereas StackOverflow publishes “LastUpdated” information (e.g. the most recent answer/edit of a question) in its RSS feeds, hubfs does not (it only has the original publishing date – e.g. the date a question was first asked).  Thus I need to use a different comparator for sorting items from each of these feeds.  I define a couple functions to project out the field I want to use for comparing items for new-ness.

let PublishDate (item:SyndicationItem) = item.PublishDate 
let LastUpdate (item:SyndicationItem) = item.LastUpdatedTime 

let feeds = [|
    "http://stackoverflow.com/feeds/tag/f%23",                   "F#",                     LastUpdate, soIcon, true
    "http://stackoverflow.com/feeds/tag/functional-programming", "Functional Programming", LastUpdate, soIcon, true
    "http://stackoverflow.com/feeds/tag/recursion",              "Recursion",              LastUpdate, soIcon, true
    "http://cs.hubfs.net/forums/aggregaterss.aspx?Mode=2",       "HubFS",                  PublishDate, hubfsIcon, true 
    "http://stackoverflow.com/feeds/tag/c%23",                   "C#",                     LastUpdate, soIcon, false            
    "http://stackoverflow.com/feeds/tag/lambda",                 "lambda",                 LastUpdate, soIcon, true
    |]

Then I have a big array of all the feeds, where each item is a tuple of the URL, the title to appear in the UI, the comparison-projection function (just described), the icon to appear in the UI, and a boolean that says whether to speak question titles aloud.

Some WPF helpers

My user interface is basically a Grid of Grids of Grids.  :)  So of course I define a helper function and extension method for creating and positioning grids and their elements.

// WPF helpers
let MakeGrid(cols, rows) =
    let auto = GridLength(1.0, GridUnitType.Auto)  // 1.0 * desired content height/width
    let grid = new Grid()
    for i in 1..rows do
        grid.RowDefinitions.Add(new RowDefinition(Height = auto))
    for i in 1..cols do
        grid.ColumnDefinitions.Add(new ColumnDefinition(Width = auto))
    grid
type System.Windows.Controls.Grid with
    member this.AddAt(col, row, element) =
        Grid.SetRow(element, row)
        Grid.SetColumn(element, col)
        this.Children.Add(element) |> ignore

type System.Windows.UIElement with
    member this.WrappedInBorderWithThickness(borderThickness) =
        new Border(BorderThickness = Thickness(borderThickness), Child = this)
    member this.WrappedInBorderWithThicknessAndColor(borderThickness, brush) =
        new Border(BorderThickness = Thickness(borderThickness), BorderBrush = brush, Child = this)

My UI also uses borders, so I define a couple useful extension methods for that.

How long ago?

In order to have pretty “ago” information, like “1 hour ago” or “6 minutes ago” or whatever, we need to code it up:

// other helpers
let Ago(date : DateTimeOffset) = 
    let diff = DateTimeOffset.Now - date
    if diff < TimeSpan.FromHours(1.0) then
        match diff.Minutes with
        | 1 -> "1 minute ago"
        | x -> sprintf "%d minutes ago" x
    elif diff < TimeSpan.FromDays(1.0) then
        match diff.Hours with
        | 1 -> "1 hour ago"
        | x -> sprintf "%d hours ago" x
    else
        match diff.Days with
        | 1 -> "1 day ago"
        | x -> sprintf "%d days ago" x

So now I can pass in e.g. a LastUpdatedTime and get back a pretty “ago” string.

Link coloring

The all-important hyperlink coloring:

let visitedLinkDiscoverer = new DiscoverIEVisitedLinks.IEVisitedLinksDiscoverer()

let LinkColor(uri) = 
    if visitedLinkDiscoverer.IsUriVisited(uri) 
    then Brushes.Purple 
    else Brushes.Blue

Opening a browser

This is the scariest part of the app.  I want to open a new tab in my existing IE browser instance, so that I will have all my cookies and login credentials and such.  There’s any easy way to do this: Process.Start().  But that’s scary, because it basically says “run this string”.  And the string is data I just got from some random location on the web!  Your security spidey-sense ought to be tingling; mine is.  In the end, I just try very hard to ensure the string looks very much like a simple URL, so that I feel good that calling Process.Start on the string will just open a browser tab.  But I am interested to hear if people know a better/safer way to do this!

let OpenInBrowser (uri:string) =
    // Check belows is my attempt to validate input (link from rss feed) from accidentally running arbitrary code on 
    // this box (Process.Start).  Would be great to think more about security implications.
    if not(uri.StartsWith("http://")) then
        Debug.Assert(false, sprintf "Uri '%s' does not start with http://, so not trying to open it" uri)
    else
        try
            let parsedUri = (new Uri(uri)).AbsoluteUri // ensure legal Uri, else will throw
            // Process.Start uses my existing browser window (opens a new tab), rather than starting new window, 
            // which is good because existing browser window has my cookies/login/credentials.
            Process.Start(parsedUri) |> ignore  
        with e ->
            Debug.Assert(false, sprintf "Uri '%s' did not parse, or Process.Start failed" uri)

When new items arrive

This code has the main bit of ‘update logic’.  I have some old items (along with data about whether they’re yellow-highlight-new and whether they need to be announced), and some new data from a recent poll of the RSS feed, and now I have to compute what are the newest items that should be displayed.  The comparison-projection function (as described above) is passed in as a parameter to correctly sort new items by new-ness.

// Item update logic
let ComputeNextItems(compProjection, oldItems:seq<SyndicationItem*bool*bool>, newItems:seq<SyndicationItem>) =
    // 1st bool represents whether item is new to the user (highlight), 2nd whether needs to be announced (new to speech)
    let newItems = newItems |> Seq.toArray 
    // sort by newness
    newItems |> Array.sortInPlaceWith (fun x y -> compare (compProjection y) (compProjection x))
    // remove duplicate ids (e.g. may have two items with same Id but different LastUpdatedTimes, keep most recent)
    let newItems = newItems |> Seq.distinctBy (fun x -> x.Id) |> Seq.toArray 
    // remove any old items that have since been updated
    let nonUpdatedOldItems = 
        oldItems |> (newItems |> Seq.fold (fun f item -> f << Seq.filter (fun (x,_,_) -> x.Id <> item.Id)) id)
    // remove enough more olds (the oldest ones, at the end of the list) to have room for all the new ones
    let numOldToKeep = max 0 (NumItemsPerFeed - newItems.Length)
    let oldKeepers = Seq.take numOldToKeep nonUpdatedOldItems
    let newKeepers = Seq.take (NumItemsPerFeed - numOldToKeep) newItems
    Seq.append (newKeepers |> Seq.map (fun x -> x,true,true)) oldKeepers |> Seq.toArray 

Note the interesting use of Seq.fold to build up a filtering function that filters out any oldItems that match any of the newItems’ Ids.

Little text helpers

Sometimes the titles have these escaped bits of HTML, so I unescape them…

let WashText (text:string) =
    text.Replace("&quot;", "\"")
        .Replace("&amp;", "&")
        .Replace("&lt;", "<")
        .Replace("&gt;", ">")

let ShortenTitle(title:string) =
    match title.LastIndexOf(' ') with
    | -1 -> title
    | i -> title.Substring(0,i)+"..."

…and some titles are too long to display, so I find word boundaries to (progressively) shorten long titles with an ellipsis (see below).

Filling a grid with the most recent items

Ok, now we’re getting to the meat.  I have an array of item data, and I need to draw the data portion of the grid:

GridData

and possibly also announce some titles.  Here’s the code:

let rec PopulateGridItems(items : array<SyndicationItem*bool*bool>, grid:Grid, shouldSpeak) =
    for i in 0..items.Length-1 do
        let item,isNewForHighlight,isNewForSpeech = items.[i]
        let uri = item.Links.[0].Uri
        let MakeRow(title) = 
            let link = new Hyperlink(new Run(title), NavigateUri = uri, Foreground = LinkColor(uri.AbsoluteUri))
            link.Click.Add(fun _ -> Async.StartImmediate( async {
                    OpenInBrowser uri.AbsoluteUri
                    do! Async.Sleep(2000) // give browser a chance to open the link, so will be purple on redraw
                    grid.Children.Clear()
                    PopulateGridItems(items, grid, shouldSpeak)    }))
            let row = new TextBlock(FontSize = 16.0)
            if isNewForHighlight then
                row.Background <- Brushes.Yellow 
            row.Inlines.Add(link)
            row.Inlines.Add(new Run("  " + Ago(item.PublishDate)))
            row
        let rec FitTitleToReasonableWidth(curTitle) =
            let row = MakeRow(curTitle)
            grid.AddAt(0, i, row) |> ignore
            row.UpdateLayout()
            if row.ActualWidth > 650.0 then
                let newTitle = ShortenTitle(curTitle)
                if newTitle <> curTitle then
                    grid.Children.Remove(row)
                    FitTitleToReasonableWidth(newTitle)
        let title = item.Title.Text |> WashText
        if isNewForSpeech && shouldSpeak then
            Speech.EventuallySpeak(title)
            items.[i] <- item,isNewForHighlight,false
        FitTitleToReasonableWidth(title)

I loop over all the items.  I grab the link uri.  The MakeRow() function makes a single row, with a given title.  The row has a Hyperlink followed by a Run of text, and this all may get a yellow highlight.  When you click the link, the Click handler will open the uri in a browser, wait a couple seconds, and then redraw this portion of the UI (so as to update the purple-ness of the link that was just clicked).  Note the nifty use of ‘async’ here – I use Async.StartImmediate, which means all the code runs on the UI thread (where the Click handler starts), but I can still do non-blocking sleeps that don’t jam up the UI.  This is one way that F# async just blows the doors off everything else.

FitTitleToReasonableWidth(): I don’t know an easy way to predict how wide the rendered text will look; it depends on the exact text of the title, the font, kerning, etc.  So I just create the row with a given title, lay it out, and then see what its actual width would be.  If it’s too wide, I shorten the title and try again.  Simple enough.

I “wash” the title text, announce it if necessary, and then make an appropriate-sized row for it.

Main window, part one

I haven’t written many UI apps, but the ones I’ve written seem to have a curious feature in common: they don’t have any methods; they just do all the work in the constructor (which typically includes code to set up various event-handlers, which are just inline lambdas).  I’m not quite sure what to make of this, but thought I’d call it out.  Anyway, this UI app is no different.  I create my own window type, and its constructor is effectively two lines long.  I set the title, and then I add a handler for the Initialized event.  The handler just happens to be a 50-line lambda.  :)  Here’s the start:

// Main UI        
type MyWindow() as this =
    inherit Window() 
    do
        this.Title <- "RSS Dashboard"
        this.Initialized.Add(fun _ ->
        let wholeWindow = MakeGrid(1,2)
        this.Content <- wholeWindow
        let grid = MakeGrid(2,3)
        wholeWindow.AddAt(0, 1, grid.WrappedInBorderWithThicknessAndColor(1.0, Brushes.Black))
        let markAllAsSeenButton = new Button(Content = new TextBlock(Text="Mark all as seen"))
        wholeWindow.AddAt(0, 0, markAllAsSeenButton)
        // We don't have the SynchronizationContext in the constructor, which is why the whole rest 
        // of the method waits for the Initialized event to run
        let syncContext = System.Threading.SynchronizationContext.Current 

The window content is a grid with two rows – the button at the top, and the rest, which is itself another 2×3 grid of each feed.  I grab the SynchronizationContext, which is basically a handle that allows you to ‘get on the UI thread’ – we’ll see why in a moment.

Main window, part two

We’re going to make 6 feed grids, and so I define a function to make a grid for a single feed.  It takes as parameters the five pieces of data about each feed (from the array-of-feed-info we defined at the top of the program):

        let MakeFeedGrid(feedUrl:string, topicName, compProjection, topicIcon, shouldSpeak) =
            // upper is 'title bar' of grid
            let upper = MakeGrid(2,1)
            upper.AddAt(0, 0, new Image(Source = topicIcon, Height = 16.0, Width = 16.0))
            let title = new TextBlock(FontSize = 20.0)
            title.Inlines.Add(new Underline(new Run(topicName)))
            upper.AddAt(1, 0, title.WrappedInBorderWithThickness(3.0))
            // lower is 'content portion' of grid
            let lower = MakeGrid(1,NumItemsPerFeed)
            let items = ref [||]  // the main mutable state - only read/update this on the UI thread
            let Redraw() = lower.Children.Clear(); PopulateGridItems(!items, lower, shouldSpeak)
            let feedSrc = new ObservableFeed.FeedSource(feedUrl, FeedUpdateFrequency)
            let obs = feedSrc.AllUpdates |> ObservableFeed.Batch 
            obs.Subscribe(fun newItems -> syncContext.Post(fun _ ->
                let firstTime = (!items).Length = 0
                items := ComputeNextItems(compProjection, !items, newItems)
                if firstTime then
                    // first time, mark nothing as new to user (no highlights, nothing to announce)
                    items := !items |> Array.map (fun (i,_,_) -> i,false,false)
                // TODO what if fewer than desired number of items?  indeed this fails sometimes
                Debug.Assert((!items).Length = NumItemsPerFeed, "TODO fix this")
                Redraw()
            , null)) |> ignore
            markAllAsSeenButton.Click.Add(fun _ -> 
                items := !items |> Array.map (fun (i,_,b) -> i,false,b)
                Redraw())
            async { 
                while true do
                    do! Async.Sleep(60000)  // every minute...
                    Redraw()  // ... redraw, so as to recompute e.g. '3 minutes ago' to be current in the display
            } |> Async.StartImmediate 
            feedSrc.Start()
            // combine upper and lower
            let grid = MakeGrid(1,2)
            grid.AddAt(0, 0, upper.WrappedInBorderWithThickness(5.0))
            grid.AddAt(0, 1, lower.WrappedInBorderWithThickness(5.0))
            grid.WrappedInBorderWithThickness(5.0).WrappedInBorderWithThicknessAndColor(1.0, Brushes.Black)

The grid for each feed has two parts – an ‘upper’ part which is the title bar (itself a 2×1 grid containing an icon and the title), and a ‘lower’ part which contains the ‘grid items’ we saw in PopulateGridItems().  The ‘backing store’ for the items is a mutable reference to an array.  (All the code runs on the UI thread, so we don’t need to worry about locking the mutable data.)  For each feed grid, we create a FeedSource object for this URL that will poll with the FeedUpdateFrequency we defined in the constants at the top.  We grab the AllUpdates observable, Batch those up, and Subscribe with a handler for newItems.  The FeedSource runs and fires events from an arbitrary background thread, so this is the one place in the main program where we’re off the UI thread.  We get back on the UI thread by calling SynchronizationContext.Post.  The very first time we visit each feed, everything will appear ‘new’, so we go flip the newness booleans to false the first time around (to avoid having everything get a yellow highlight and having every title announced when the app first starts up).  Otherwise, each time the FeedSource fires new data at us, we use it to compute the next set of items to display and redraw this portion of the display.  (Once upon a time there was a bug here; I may have fixed it, but I left the assert I was using to catch it.)

I attach a handler to the “mark all as seen” button.  If you click the button, I flip all the ‘yellow highlight’ booleans for this feed back to false, and then redraw.

Our “ago” information (“3 minutes ago”) will quickly get stale if we don’t refresh it, so we start a loop that redraws every minute.  Once again, F# async kicks butt, as I can just write this as though it were a synchronous loop running on the UI thread, but the non-blocking sleep call ensures that the UI stays live.  Awesome.

Since we have already done all the subscribing to the IObservable and wiring things up, we can go ahead and tell the FeedSource to Start().

I finish by putting the upper and lower portions of the UI together into a grid that gets returned.

Main window, part three

Now that I have the MakeFeedGrid function, I just call it for each feed:

        grid.AddAt(0, 0, MakeFeedGrid(feeds.[0]))
        grid.AddAt(0, 1, MakeFeedGrid(feeds.[1]))
        grid.AddAt(0, 2, MakeFeedGrid(feeds.[2]))
        grid.AddAt(1, 0, MakeFeedGrid(feeds.[3]))
        grid.AddAt(1, 1, MakeFeedGrid(feeds.[4]))
        grid.AddAt(1, 2, MakeFeedGrid(feeds.[5]))
        )
    
[<STAThread>] 
do  
    let app =  new Application() 
    app.Run(new MyWindow()) |> ignore 

(The close-paren ends the Initialized handler.)  Now I kick off the WPF app.  That’s it, we’re done!

Summary

In less than 500 lines of code, we’ve created a useful little app for keeping up with RSS feeds.  Along the way we leveraged a variety of different technologies, including WCF (both for SyndicationFeeds and for a mini-web-server), IObservables (for a background feed-polling eventing model), speech synthesis (to announce question titles as new data arrives), COM interop and some crazy Javascript (to get link coloring info from IE), and WPF (to draw the user interface).  Despite the fact that work happens on a variety of different threads, we didn’t have to use low-level APIs like Thread or BackgroundWorker.  Rather, we used a MailboxProcessor to create an agent to queue up messages to the speech synthesizer, and used F# asyncs both for writing apparently-single-threaded code with non-blocking calls and sleeps, as well as for simple fork-join parallelism.

I really enjoyed putting together a small-but-non-trivial, useful app that spanned so many concepts and technologies.  I hope you enjoyed reading about it.

Full Source Code

In addition to enjoying reading the blog series, you’ll enjoy playing with the code yourself, so here it all is.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: