F# & WPF:基本的 UI 更新
Posted
技术标签:
【中文标题】F# & WPF:基本的 UI 更新【英文标题】:F# & WPF: basic UI update 【发布时间】:2017-07-28 18:21:12 【问题描述】:我刚开始使用 WPF。我为我的文件处理脚本 (F#) 制作了一个拖放 UI。如何更新 textBlock 以提供进度反馈?当前版本中的 UI 仅在处理完所有文件后更新。我需要定义一个 DependencyProperty 类型并设置一个绑定吗? 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
【问题讨论】:
您需要在后台线程上执行循环和休眠,以便 UI 能够同时更新。我不是 F# 开发人员,所以我无法帮助您完成这部分:(。但您可以参考以下链接获取一些 C# 示例:***.com/questions/42165688/…***.com/questions/41270570/… 这篇文章很好地介绍了在 F# 中做异步的东西:fsharpforfunandprofit.com/posts/concurrency-async-and-parallel 【参考方案1】:通过在 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 完成相同的脚本,并使其更易于以后扩展。如果您使用 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>
请注意,我在这里“改进”了 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
请注意,这是通过为绑定创建“视图模型”来实现的。我将逻辑移到 ViewModel 中(这很常见),然后使用 FsXaml 从 Xaml 创建 Window,vm
用作窗口的 DataContext。这将为您“连接”任何绑定。
使用单个绑定,这更加冗长 - 但是随着您扩展 UI,好处会很快变得更加明显,因为添加属性很简单,并且使用 XAML 与尝试在代码中设置样式相比,样式变得更加简单。例如,如果您开始使用集合,则很难在代码中创建正确的模板和样式,但在 XAML 中却很简单。
【讨论】:
感谢 Reed 的这个非常有帮助的回答!我没有找到关于 FSharp.ViewModule 的任何教程。您也可以添加一个示例吗? @Goswin 当然 - 我也会坚持使用 FsXaml - 因为它可以更好地加载 xaml ;)【参考方案2】:您发布的示例的问题是您正在 UI 线程上运行处理。如 cmets 中所述,在 F# here 中有很好的异步处理教程。
完成此操作后,您会遇到另一个问题:您无法从后台线程更新 UI。您需要在 UI 线程上“调用”更新,而不是直接从后台线程更新 UI。详情请见here。
【讨论】:
以上是关于F# & WPF:基本的 UI 更新的主要内容,如果未能解决你的问题,请参考以下文章