2017-03-08 49 views
4

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

这里是我当前的应用程序转换为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 
+2

您需要在后台线程上执行循环和休眠,以便UI能够同时更新。我不是一个F#开发人员,但我不能帮你做这个部分:(但是你可以参考下面的链接获取一些C#示例: http://stackoverflow.com/questions/42165688/to-avoid-我可以使用枚举文件里面dispatcher调用o/42165962#42165962 http://stackoverflow.com/questions/41270570/adding-an-item-toa-a- listcustomclass-which-databinded-to-a-datagrid/41271067#41271067 – mm8

+2

本文介绍如何在F#中完成异步内容:https://fsharpforfunandprofit.com/posts/concurrency-async-and-parallel/ –

回答

4

通过在UI线程上调用Threading.Thread.Sleep 300,可以阻止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 

注意,它也可以通过数据绑定来做到这一点。为了绑定(并让它更新),你需要绑定到一个“视图模型” - 一些实现了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 

注意,这可能是简化如果你想使用像FSharp.ViewModule(使创建INotifyPropertyChanged部分更好)。

编辑:

此相同的脚本可以使用XAML和FSharp.ViewModule来完成,并使其更容易扩展以后。如果您使用的一揽子贷款引用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> 

注意,我在这里“改进”了用户界面 - 它将文本块包装在滚动查看器中,设置大小,并在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 

请注意,这是通过创建一个“视图模式”的结合。我将逻辑移入ViewModel(这很常见),然后使用FsXaml从Xaml创建窗口,并将vm用作窗口的DataContext。这将为您绑定任何绑定。

使用单个绑定时,这是比较冗长的 - 但随着您对UI的扩展,其好处变得非常快速,因为添加属性很简单,并且在使用XAML和尝试在代码中设置样式时,样式变得更加简单。例如,如果您开始使用集合,那么在代码中创建适当的模板和样式非常困难,但在XAML中无足轻重。

+0

谢谢你这个非常有帮助的答案Reed!我没有在FSharp.ViewModule上找到任何教程,你可以添加一个例子吗? – Goswin

+2

@Goswin当然 - 我也会坚持使用FsXaml - 因为它会加载xaml更好;) –

4

与您发布的示例中的问题是,你在UI线程上运行的处理。正如评论中指出的那样,在F#here中有很好的教程来做异步处理。

一旦你这样做了,你会遇到另一个问题:你不能从后台线程更新UI。不需要直接从后台线程更新UI,您需要在UI线程上“调用”更新。关于这方面的细节可以在here找到。