F # and WPF: Basic UI Update

I just started using WPF. I made a drag and drop interface for file processing script files (F #). How can I update textBlock to get feedback on the results? The user interface in the current version is updated only after processing all the files. Do I need to determine the type of DependencyProperty and set the binding? What would be the minimum version of this in F #?

Here is my current application converted to 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 
+5
source share
2 answers

By calling Threading.Thread.Sleep 300 in the user interface thread, you block the Windows message pump and prevent any updates from occurring in this thread.

The easiest way to handle this is to move everything to the async workflow and perform the update in the background thread. However, you will need to update the user interface in the main thread. async workflow, you can switch directly between each other.

This requires a few small changes:

 #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 this is also possible with data binding. To bind (and update it), you need to bind to a "view model" - some type that implements INotifyPropertyChanged , and then creates a binding (which is ugly in code). The problem with the user interface thread is somewhat simplified - you still need to push work away from the user interface thread, but when binding to a simple property, you can set the value for other threads. (If you are using a collection, you still need to switch to the UI thread.)

An example converted to using binding would be something like this:

 #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 can be simplified if you want to use something like FSharp.ViewModule (makes creating the INotifyPropertyChanged part much nicer).

Edit:

The same script can be executed using XAML and FSharp.ViewModule and simplify its extension later. If you use paket to refer to FSharp.ViewModule.Core and FsXaml.Wpf (latest version), you can transfer the user interface definition to a XAML file (assuming the name MyWindow.xaml ), for example:

 <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 have “improved” the user interface here - it wraps the text block in the scroll viewer, sets the size, and declares the binding and event handler in XAML instead of code. You can easily expand this with more bindings, styles, etc.

If you place this file in the same place as your script, you can 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 the binding. I moved the logic to a ViewModel (which is shared), and then use FsXaml to create a window from Xaml, and vm used as the DataContext of the window. This will “bind” any bindings for you.

With one binding, this is more detailed, but as the user interface expands, the benefits become much clearer very quickly, as adding properties is simple, and styling becomes much easier when using XAML and trying to style the code. For example, if you start using collections, it is incredibly difficult to create the right patterns and styles in your code, but trivial in XAML.

+5
source

The problem with the example you cited is that you are processing in a user interface thread. As noted in the comments, there is a good guide to handling asynchronous processing in F # here .

Once you do this, you will encounter another problem: you cannot update the user interface from the background thread. Instead of directly updating the user interface from a background thread, you need to “invoke” your update in the user interface stream. Details on this can be found here .

+4
source

Source: https://habr.com/ru/post/1265169/


All Articles