Build a cancellable non-blocking background worker - winforms

Let's say I have a long-running calculation that I want to run in the background as not to block the UI thread. So I am going wrap it in an asynchronous computation.
/// Some long-running calculation...
let calculation arg = async{
do! Async.Sleep 1000
return arg }
Next I need to run the calculation in a loop where I switch to another thread to execute it and back to the UI thread to do something with its results.
/// Execute calculation repeatedly in for-loop and
/// display results on UI thread after every step
open System.Threading
let backgroundLoop uiAction = async {
let ctx = SynchronizationContext.Current
for arg in 0..100 do
do! Async.SwitchToThreadPool()
let! result = calculation arg
do! Async.SwitchToContext ctx
uiAction result }
Then this loop must be wrapped in another asynchronous computation to provide some means to cancel it from the UI.
/// Event-controlled cancellation wrapper
let cancelEvent = new Event<_>()
let cancellableWorker work = async {
use cToken = new CancellationTokenSource()
Async.StartImmediate(work, cToken.Token)
do! Async.AwaitEvent cancelEvent.Publish
cToken.Cancel() }
It seems now that I have implemented a functionality similar to that of BackgroundWorker. Testing it:
// Demo where the results are added to a ListBox.
// The background calculations can be stopped
// by a keypress when the ListBox has focus
open System.Windows.Forms
let fm = new Form()
let lb = new ListBox(Dock = DockStyle.Fill)
fm.Controls.Add lb
fm.Load.Add <| fun _ ->
backgroundLoop (lb.Items.Add >> ignore)
|> cancellableWorker
|> Async.StartImmediate
lb.KeyDown.Add <| fun _ ->
cancelEvent.Trigger()
[<System.STAThread>]
#if INTERACTIVE
fm.Show()
#else
Application.Run fm
#endif
It appears to be a bit of effort for something which I imagine as a relatively common workflow. Can we simplify it? Am I missing anything crucial?

Then this loop must be wrapped in another asynchronous computation to provide some means to cancel it from the UI.
Can we simplify it?
I think cancelEvent and cancellableWorker are unnecessary indirection in this case. You can use a CancellationTokenSource and cancel it directly from a UI event, instead of an Event<> that in turn cancels a token.
let calculation arg = async {
do! Async.Sleep 1000
return arg }
open System.Threading
let backgroundLoop uiAction = async {
let ctx = SynchronizationContext.Current
for arg in 0..100 do
do! Async.SwitchToThreadPool()
let! result = calculation arg
do! Async.SwitchToContext ctx
uiAction result }
open System.Windows.Forms
let fm = new Form()
let lb = new ListBox(Dock = DockStyle.Fill)
fm.Controls.Add lb
let cToken = new CancellationTokenSource()
fm.Load.Add <| fun _ ->
Async.StartImmediate (backgroundLoop (lb.Items.Add >> ignore), cToken.Token)
lb.KeyDown.Add <| fun _ -> cToken.Cancel()
Also (if you haven't already) take a look at Tomas Petricek's article on non-blocking user-interfaces in F#.

Related

Observable do not call onComplete (sqlbrite - mapToOneOrDefault)

I have select from sqlbrite db but observable do not call onComplete for some reason.
My code:
fun BriteDatabase.selectDayTimelineRecord(dayTimestamp: Long) =
createQuery(table_timeline, selectWDayRecords(dayTimestamp)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.mapToOneOrDefault(StatItem(dayTimestamp, 0)) { cursor -> cursor.mapTimelineStat() }
and then I try variants:
working but I need to keep order so I can not use flatmap
Observable.fromIterable(0..6).flatMap{
db.selectDayTimelineRecords(timestampOfDayInWeek(it))
}.buffer(7).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe {}
not working (not delivery result but I do not know why)
Observable.fromIterable(0..6).concatMap{
db.selectDayTimelineRecords(timestampOfDayInWeek(it))
}.buffer(7).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe {}
As result I want Observable with list of T...
Anyone know what am I doing wrong?
I am not very familiar with SQLBrite but createQuery supposed to be keep notifying database changes. If you want to get value just once then you can use take() operator.
fun BriteDatabase.selectDayTimelineRecord(dayTimestamp: Long) =
createQuery(table_timeline, selectWDayRecords(dayTimestamp))
.mapToOneOrDefault(StatItem(dayTimestamp, 0)) { cursor -> cursor.mapTimelineStat() }
.take(1)
Then your concatMap implementation will work.

fast-rendering array of CALayers vs array of CAShapeLayers in swift

I'm writing an app in which I need to populate 500 or so layers with previously-defined bezier paths and I'm running into either PERFORMANCE or INTERACTIVITY issues depending on which route I choose. Note that I require no animation features, as the paths i draw are static:
If I use CALayers, drawing the paths to screen takes about 15 seconds (bad), but the resulting interactive experience (i.e moving around the screen) is great.
If I use CAShapeLayers, drawing the paths to screen takes a fraction of a second (good), but the interactivity is terrible.
This is my code with CALayers:
func drawPathToCALayer (myView: UIImageView, pointArray: [CGPoint], bbox: CGRect, color: UIColor) {
// step 1. create path
let path = CGPathCreateMutable()
var pathOffset = CGAffineTransformMakeTranslation( (bbox.origin.x * -1), (bbox.origin.y * -1))
CGPathAddLines(path, &pathOffset, pointArray, pointArray.count)
CGPathCloseSubpath(path)
// step 2. draw to context -- this is the part that kills the amount of time that we call this function repeatedly
UIGraphicsBeginImageContextWithOptions(bbox.size, false, 0)
CGContextAddPath(UIGraphicsGetCurrentContext(), path)
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), color.CGColor)
CGContextFillPath(UIGraphicsGetCurrentContext())
// step 3. assign context drawing to sublayer
let sublayer = CALayer()
let strokeImage = UIGraphicsGetImageFromCurrentImageContext()
sublayer.frame = bbox
sublayer.contents = strokeImage.CGImage
UIGraphicsEndImageContext()
myView.layer.addSublayer(sublayer)
This is the code with CAShapeLayers
func drawPathToCAShapeLayer (myView: UIImageView, pointArray: [CGPoint], bbox: CGRect, color: UIColor) {
// step 1. create path
let path = CGPathCreateMutable()
var pathOffset = CGAffineTransformMakeTranslation( (bbox.origin.x * -1), (bbox.origin.y * -1))
CGPathAddLines(path, &pathOffset, pointArray, pointArray.count)
CGPathCloseSubpath(path)
// step 2. assign path to sublayer
let sublayer = CAShapeLayer()
sublayer.path = path
sublayer.fillColor = color
myView.layer.addSublayer(sublayer)
I like the succinctness and speed of the CAShapeLayer approach, but from an interactivity point of view, this route is a no go.
The question is (thanks for hanging in there), is there is a way to do a hybrid approach in which I draw to a CAShapeLayer temporarily, and use it to populate a CALayer like so?
func drawPathToHybrid (myView: UIImageView, pointArray: [CGPoint], bbox: CGRect, color: UIColor) {
// step 1. create path
let path = CGPathCreateMutable()
var pathOffset = CGAffineTransformMakeTranslation( (bbox.origin.x * -1), (bbox.origin.y * -1))
CGPathAddLines(path, &pathOffset, pointArray, pointArray.count)
CGPathCloseSubpath(path)
// step 2. assign path to sublayer
let sublayer = CALayer()
let tmplayer = CAShapeLayer()
tmplayer.path = path
tmplayer.fillColor = color
sublayer.contents = tmplayer.contents // ---> i know this doesn't work, but is there something similar I can take advantage of that doesn't rely on defining a context?
myView.layer.addSublayer(sublayer)
Or better yet, is there some other way that I can populate an array of CALayers with bezier paths to get both good INTERACTIVITY and PERFORMANCE?

Unable to create array of SKActions

I'm experimenting with SpriteKit in Swift but somehow seem unable to create an array of actions to use in a sequence. I've split it up to try and pin-point the problem, but no luck so far.
func animateBackground(){
let moveLeft = SKAction.moveByX(100, y: 0, duration: 3)
moveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let moveRight = SKAction.reversedAction(moveLeft)
let actions = [moveLeft, moveRight] // <--- here there be dragons/trouble
let sequence = SKAction.sequence(actions)
let repeat = SKAction.repeatActionForever(sequence)
}
When trying to create the actions-array I get the error "Cannot convert the expression's type 'Array' to type 'ArrayLiteralConvertible' " So, I thought I might need to be more explicit and attempted to change it to
var actions: SKAction[] = [moveLeft, moveRight]
This seemed to bring down the house, and not in a good way, resulting in the SourceKit terminated bug...
You're adding a function to the array for moveRight, not the SKAction itself. Try using this instead:
let moveRight = SKAction.reversedAction(moveLeft)()
When you create moveRight you're actually generating a function. You can call the function with "()" to get the actual SKAction. I added explicit types to the two SKAction's so it's clear that they can be put in an SKAction[]:
let moveLeft:SKAction = SKAction.moveByX(100, y: 0, duration: 3)
moveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let moveRight:SKAction = moveLeft.reversedAction()
let actions = [moveLeft, moveRight]
let sequence = SKAction.sequence(actions)
let repeat = SKAction.repeatActionForever(sequence)

Create button handler in F# for WinForms app

I'm trying to make a very simple application in F#. I want just to make a tiny button with the click event, which will call the MessageBox.Show() method.
open System
open System.Windows.Forms
module FSI =
[<System.Runtime.InteropServices.DllImport("user32.dll")>]
extern bool ShowWindow(nativeint hWnd, int flags)
let HideConsole() =
let proc = System.Diagnostics.Process.GetCurrentProcess()
ShowWindow(proc.MainWindowHandle, 0)
FSI.HideConsole()
let form1 = new Form()
form1.Text <- "App"
let Test() = ( MessageBox.Show("Clicked.") )
let button1 = new Button( Text = "Click Me" )
button1.Click.AddHandler(fun _ _ -> Test())
form1.Controls.Add(button1)
[<STAThread>]
do
Application.Run(form1)
The errors occurred for the:
FSI.HideConsole() // warning
button1.Click.AddHandler(fun _ _ -> Test()) // compiler error
And they are similar.
For the 1-st:
Warning This expression should have type 'unit', but has type 'bool'. Use 'ignore' to discard the result of the expression, or 'let' to bind the result to a name.
For the 2-nd:
This expression was expected to have type unit but here has type DialogResult
F# functions implicitly return the result of their last expression, so currently your function Test() returns the result of MessageBox.Show(string). To create a function that returns void/unit, you can use the ignore operator.
let Test() = ignore (MessageBox.Show("Clicked."))
It's a bit cumbersome to have to do the extra parenthesis wrapping, so typically one uses the pipeforward (|>) operator, which doesn't require that:-
let Test() = MessageBox.Show("Clicked.") |> ignore
This can also be applied to your call of HideConsole. The compiler uses the warning to inform you that the return value of HideConsole is not used.

How to use string indexing with IDataReader in F#?

I'm new to F# and trying to dive in first and do a more formal introduction later. I have the following code:
type Person =
{
Id: int
Name: string
}
let GetPeople() =
//seq {
use conn = new SQLiteConnection(connectionString)
use cmd = new SQLiteCommand(sql, conn)
cmd.CommandType <- CommandType.Text
conn.Open()
use reader = cmd.ExecuteReader()
let mutable x = {Id = 1; Name = "Mary"; }
while reader.Read() do
let y = 0
// breakpoint here
x <- {
Id = unbox<int>(reader.["id"])
Name = unbox<string>(reader.["name"])
}
x
//}
let y = GetPeople()
I plan to replace the loop body with a yield statement and clean up the code. But right now I'm just trying to make sure the data access works by debugging the code and looking at the datareader. Currently I'm getting a System.InvalidCastException. When I put a breakpoint at the point indicated by the commented line above, and then type in the immediate windows reader["name"] I get a valid value from the database so I know it's connecting to the db ok. However if I try to put reader["name"] (as opposed to reader.["name"]) in the source file I get "This value is not a function and cannot be applied" message.
Why can I use reader["name"] in the immediate window but not in my fsharp code? How can I use string indexing with the reader?
Update
Following Jack P.'s advice I split out the code into separate lines and now I see where the error occurs:
let id = reader.["id"]
let id_unboxed = unbox id // <--- error on this line
id has the type object {long} according to the debugger.
Jack already answered the question regarding different syntax for indexing in F# and in the immediate window or watches, so I'll skip that.
In my experience, the most common reason for getting System.InvalidCastException when reading data from a database is that the value returned by reader.["xyz"] is actually DbNull.Value instead of an actual string or integer. Casting DbNull.Value to integer or string will fail (because it is a special value), so if you're working with nullable columns, you need to check this explicitly:
let name = reader.["name"]
let name_unboxed : string =
if name = DbNull.Value then null else unbox name
You can make the code nicer by defining the ? operator which allows you to write reader?name to perform the lookup. If you're dealing with nulls you can also use reader?name defaultValue with the following definition:
let (?) (reader:IDataReader) (name:string) (def:'R) : 'R =
let v = reader.[name]
if Object.Equals(v, DBNull.Value) then def
else unbox v
The code then becomes:
let name = reader?name null
let id = reader?id -1
This should also simplify debugging as you can step into the implementation of ? and see what is going on.
You can use reader["name"] in the immediate window because the immediate window uses C# syntax, not F# syntax.
One thing to note: since F# is much more concise than C#, there can be a lot going on within a single line. In other words, setting a breakpoint on the line may not help you narrow down the problem. In those cases, I normally "expand" the expression into multiple let-bindings on multiple lines; doing this makes it easier to step through the expression and find the cause of the problem (at which point, you can just make the change to your original one-liner).
What happens if you pull the item accesses and unbox calls out into their own let-bindings? For example:
while reader.Read() do
let y = 0
// breakpoint here
let id = reader.["id"]
let id_unboxed : int = unbox id
let name = reader.["name"]
let name_unboxed : string = unbox name
x <- { Id = id_unboxed; Name = name_unboxed; }
x

Resources