F#和WPF:基本的UI更新

Gos*_*win 6 wpf f#

我刚开始使用WPF.我为我的文件处理脚本(F#)拖放了UI.如何更新textBlock以提供进度反馈?当前版本中的UI仅在处理完所有文件后更新.我是否需要定义DependencyProperty类型并设置Binding?F#中的最小版本是什么?

这是我当前的应用程序转换为F#脚本:

#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
Run Code Online (Sandbox Code Playgroud)

Ree*_*sey 6

通过调用Threading.Thread.Sleep 300UI线程,您可以阻止Windows消息泵,并防止在该线程上发生任何更新.

处理此问题的最简单方法是将所有内容移动到async工作流中,并在后台线程上执行更新.但是,您需要更新主线程上的UI.在async工作流程中,您可以直接来回切换.

这需要对您的代码进行一些小的更改才能工作:

#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
Run Code Online (Sandbox Code Playgroud)

请注意,也可以通过数据绑定来完成此操作.为了绑定(并让它更新),你需要绑定到一个"视图模型" - 一些实现的类型,INotifyPropertyChanged然后创建绑定(在代码中很难看).UI线程的问题变得有点简单 - 你仍然需要将工作从UI线程推出,但是当绑定到一个简单的属性时,你可以在其他线程上设置值.(但是,如果使用集合,则仍需要切换到UI线程.)

转换为使用绑定的示例将类似于以下内容:

#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
Run Code Online (Sandbox Code Playgroud)

请注意,如果您想使用类似FSharp.ViewModule的东西(使创建INotifyPropertyChanged部分更好),可以简化这一点.

编辑:

可以使用XAML和FSharp.ViewModule完成相同的脚本,并使以后更容易扩展.如果使用paket引用FSharp.ViewModule.Core和FsXaml.Wpf(最新版本),则可以将UI定义移动到XAML文件(假设名称为MyWindow.xaml),如下所示:

<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>
Run Code Online (Sandbox Code Playgroud)

请注意,我在这里"改进"了UI - 它将文本块包装在滚动查看器中,设置大小,并在XAML中声明绑定和事件处理程序而不是代码.您可以使用更多绑定,样式等轻松扩展它.

如果将此文件放在与脚本相同的位置,则可以编写:

#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
Run Code Online (Sandbox Code Playgroud)

请注意,这可以通过创建绑定的"视图模型"来实现.我将逻辑移动到ViewModel(这是常见的),然后使用FsXaml从Xaml创建Window,并将其vm用作窗口的DataContext.这将为您"绑定"任何绑定.

使用单个绑定,这更加冗长 - 但是当您扩展UI时,优势变得更加清晰,因为添加属性很简单,使用XAML和尝试在代码中设置样式时样式变得更加简单.例如,如果你开始使用集合,那么在代码中创建适当的模板和样式是非常困难的,但在XAML中则是微不足道的.

  • @Goswin当然 - 我也会坚持使用FsXaml - 因为它使得加载xaml更好;) (2认同)