I just started to use WPF. I made a drag and drop UI for my file processing scripts (F#). How can I update the textBlock to give progress feedback? The UI in the current version only updates after processing all files. Do I need to define a DependencyProperty type and set a Binding? What would be a minimal version of that in F# ?
Here is my current app converted to a F# script:
#r "WindowsBase.dll"
#r "PresentationCore.dll"
#r "PresentationFramework.dll"
open System
open System.Windows
open System.Windows.Controls
[< STAThread >]
do
let textBlock = TextBlock()
textBlock.Text <- "Drag and drop a folder here"
let getFiles path =
for file in IO.Directory.EnumerateFiles path do
textBlock.Text <- textBlock.Text + "\r\n" + file // how to make this update show in the UI immediatly?
// do some slow file processing here..
Threading.Thread.Sleep 300 // just a placeholder to simulate the delay of file processing
let w = Window()
w.Content <- textBlock
w.Title <- "UI test"
w.AllowDrop <- true
w.Drop.Add(fun e ->
if e.Data.GetDataPresent DataFormats.FileDrop then
e.Data.GetData DataFormats.FileDrop
:?> string []
|> Seq.iter getFiles)
let app = Application()
app.Run(w)
|> ignore
By calling Threading.Thread.Sleep 300
on the UI thread, you block the windows message pump, and prevent any updates from occurring on that thread.
The simplest way to handle this would be to move everything into an async
workflow, and do the update on a background thread. However, you will need to update the UI on the main thread. Within an async
workflow, you can switch back and forth directly.
This requires a couple of small changes to your code to work:
#r "WindowsBase.dll"
#r "PresentationCore.dll"
#r "PresentationFramework.dll"
open System
open System.Windows
open System.Windows.Controls
[< STAThread >]
do
let textBlock = TextBlock()
textBlock.Text <- "Drag and drop a folder here"
let getFiles path =
// Get the context (UI thread)
let ctx = System.Threading.SynchronizationContext.Current
async {
for file in IO.Directory.EnumerateFiles path do
// Switch context to UI thread so we can update control
do! Async.SwitchToContext ctx
textBlock.Text <- textBlock.Text + "\r\n" + file // Update UI immediately
do! Async.SwitchToThreadPool ()
// do some slow file processing here.. this will happen on a background thread
Threading.Thread.Sleep 300 // just a placeholder to simulate the delay of file processing
} |> Async.StartImmediate
let w = Window()
w.Content <- textBlock
w.Title <- "UI test"
w.AllowDrop <- true
w.Drop.Add(fun e ->
if e.Data.GetDataPresent DataFormats.FileDrop then
e.Data.GetData DataFormats.FileDrop
:?> string []
|> Seq.iter getFiles)
let app = Application()
app.Run(w)
|> ignore
Note that it's also possible to do this via data binding. In order to bind (and have it update), you'd need to bind to a "view model" - some type that implements INotifyPropertyChanged
, and then create the binding (which is ugly in code). The issue with the UI thread becomes somewhat simpler - you still need to push the work off the UI thread, but when binding to a simple property, you can set the value on other threads. (If you use a collection, you still need to switch to the UI thread, however.)
The example converted to using a binding would be something like the following:
#r "WindowsBase.dll"
#r "PresentationCore.dll"
#r "PresentationFramework.dll"
#r "System.Xaml.dll"
open System
open System.Windows
open System.Windows.Controls
open System.Windows.Data
open System.ComponentModel
type TextWrapper (initial : string) =
let mutable value = initial
let evt = Event<_,_>()
member this.Value
with get() = value
and set(v) =
if v <> value then
value <- v
evt.Trigger(this, PropertyChangedEventArgs("Value"))
interface INotifyPropertyChanged with
[<CLIEvent>]
member __.PropertyChanged = evt.Publish
[< STAThread >]
do
let textBlock = TextBlock()
// Create a text wrapper and bind to it
let text = TextWrapper "Drag and drop a folder here"
textBlock.SetBinding(TextBlock.TextProperty, Binding("Value")) |> ignore
textBlock.DataContext <- text
let getFiles path =
async {
for file in IO.Directory.EnumerateFiles path do
text.Value <- text.Value + "\r\n" + file // how to make this update show in the UI immediatly?
// do some slow file processing here.. this will happen on a background thread
Threading.Thread.Sleep 300 // just a placeholder to simulate the delay of file processing
} |> Async.Start
let w = Window()
w.Content <- textBlock
w.Title <- "UI test"
w.AllowDrop <- true
w.Drop.Add(fun e ->
if e.Data.GetDataPresent DataFormats.FileDrop then
e.Data.GetData DataFormats.FileDrop
:?> string []
|> Seq.iter getFiles)
let app = Application()
app.Run(w)
|> ignore
Note that this could be simplified if you wanted to use something like FSharp.ViewModule (makes creating the INotifyPropertyChanged portion much nicer).
Edit:
This same script could be done using XAML and FSharp.ViewModule, and make it easier to extend later. If you use paket to reference FSharp.ViewModule.Core and FsXaml.Wpf (latest version), you could move the UI definition to a XAML file (assuming name of MyWindow.xaml
), like so:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Title="UI Test" AllowDrop="True" Width="500" Height="300" Drop="DoDrop">
<ScrollViewer >
<TextBlock Text="{Binding Text}" />
</ScrollViewer>
</Window>
Note that I "improved" the UI here - it's wrapping the text block in a scroll viewer, setting sizing, and declaring the binding and event handler in XAML instead of code. You can easily extend this with more bindings, styles, etc.
If you place this file in the same location as your script, you can then write:
#r "WindowsBase.dll"
#r "PresentationCore.dll"
#r "PresentationFramework.dll"
#r "System.Xaml.dll"
#r "../packages/FSharp.ViewModule.Core/lib/portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1/FSharp.ViewModule.dll"
#r "../packages/FsXaml.Wpf/lib/net45/FsXaml.Wpf.dll"
#r "../packages/FsXaml.Wpf/lib/net45/FsXaml.Wpf.TypeProvider.dll"
open System
open System.Windows
open System.Windows.Controls
open System.Windows.Data
open System.ComponentModel
open ViewModule
open ViewModule.FSharp
open FsXaml
type MyViewModel (initial : string) as self =
inherit ViewModelBase()
// You can add as many properties as you want for binding
let text = self.Factory.Backing(<@ self.Text @>, initial)
member __.Text with get() = text.Value and set(v) = text.Value <- v
member this.AddFiles path =
async {
for file in IO.Directory.EnumerateFiles path do
this.Text <- this.Text + "\r\n" + file
// do some slow file processing here.. this will happen on a background thread
Threading.Thread.Sleep 300 // just a placeholder to simulate the delay of file processing
} |> Async.Start
// Create window from XAML file
let [<Literal>] XamlFile = __SOURCE_DIRECTORY__ + "/MyWindow.xaml"
type MyWindowBase = XAML<XamlFileLocation = XamlFile>
type MyWindow () as self = // Subclass to provide drop handler
inherit MyWindowBase()
let vm = MyViewModel "Drag and drop a folder here"
do
self.DataContext <- vm
override __.DoDrop (_, e) = // Event handler specified in XAML
if e.Data.GetDataPresent DataFormats.FileDrop then
e.Data.GetData DataFormats.FileDrop :?> string []
|> Seq.iter vm.AddFiles
[< STAThread >]
do
Application().Run(MyWindow()) |> ignore
Note that this works by creating a "view model" for binding. I moved the logic into the ViewModel (which is common), then use FsXaml to create the Window from the Xaml, and the vm
is used as the DataContext of the window. This will "wire up" any bindings for you.
With a single binding, this is more verbose - but as you extend the UI, the benefits become much more clear very quickly, as adding properties is simple, and styling becomes much simpler when using XAML vs trying to style in code. If you start using collections, for example, it's incredibly difficult to create the proper templates and styles in code, but trivial in XAML.
The issue with the example you posted is that you're running the processing on the UI thread. As noted in the comments, there is good tutorial on doing async processing in F# here.
Once you've done that you'll encounter another problem: you can't update the UI from a background thread. Instead of directly updating the UI from the background thread, you'll need to 'invoke' your update on the UI thread. Details on that can be found here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With