在 FsXaml 和 ElmishWPF 中更新 ProgressBar.Value

Updating ProgressBar.Value in FsXaml and ElmishWPF

我正在尝试在 FsXaml 中更新 ProgressBar.Value。在 C# 中,我使用了下面提到的代码。我没有尝试在 F# 中实现 C# 方法,因为使用 public 字段 (myCaller) 在我看来并不是一种功能性方法(更不用说我不知道是否可以在 F# 中使用这种 C# 方法。

//C# code
namespace Special_technical_dictionary_CSharp_4._011
    {
    //...some usings
    class ExcelData
        {
         //...some code    
        public void WritingIntoDat()
            {            
            //...some code
            using (bw = new BinaryWriter(new FileStream(...some params...)))
                {
                while ((currrowIndex < (lastrowIndex + 1)))
                    {
                    //...some code                   
                    Form1.myCaller.updateProgressBarValue(100 * currrowIndex);
                    currrowIndex += 1;
                    }
                bw.Close();
                }
            //...some code 
            }
        }
    }

namespace Special_technical_dictionary_CSharp_4._011
    {
    //...some usings
    public partial class Form1 : Form
        {
        //...some code
        public static Form1 myCaller;
        
        public Form1()
            {
            InitializeComponent();
            myCaller = this;
            }
        //...some code
        public void updateProgressBarValue(int valueV)           
            => progressBar.Value = (progressBar.Value == progressBar.Maximum) ? valueV : 0;
        //...some code
        }
    }

我的问题是:F# 中用于更新 ProgressBar.Value[= 的最佳(或至少是好的)功能方法是什么(FsXaml/code 落后) 56=]?

编辑 1:

不相关的代码和文本已删除。对 Elmish.WPF 不感兴趣的人请等待,直到出现与 FsXaml 相关的答案。

编辑2:

Elmish.WPF

我尝试使用 Bent Tranberg 的评论和回答以及 his excellent example code 来处理 ProgressBar 问题。我的改编适用于 for-loop,但不适用于 List.map(i)/iter(i),它们是集合函数我实际上需要进度条。这是简化的代码:

文件:MainWindow.fs

//F# code
module Elmish.MainWindow

type ProgressIndicator = Idle | InProgress of percent: int

type Model =
    {                     
        ProgressIndicatorLeft: ProgressIndicator
        ProgressIndicatorRight: ProgressIndicator
    }

let initialModel = 
    {
        ProgressIndicatorLeft = Idle 
        ProgressIndicatorRight = Idle         
    }

let init() = initialModel, Cmd.none
    
type Msg =   
    | UpdateStatusLeft of progress: int
    | WorkIsCompleteLeft 
    | UpdateStatusRight of progress: int
    | WorkIsCompleteRight 
    | TestButtonLeftEvent
    | TestButtonRightEvent    

 // FOR TESTING PURPOSES ONLY
let private longRunningOperationLeft dispatch = //simulating long running operation
    async
        {
            for i in 1..100 do 
                do! Async.Sleep 20
                dispatch (UpdateStatusLeft i) //THIS WORKS
            dispatch WorkIsCompleteLeft
        }  
    
 // FOR TESTING PURPOSES ONLY
let private longRunningOperationRight dispatch = //simulating long running operation    
    async  //NOT WORKING
        {
            [1..10000]    
            |> List.mapi(fun i item -> 
                                     [1..100] |> List.reduce (*) |> ignore 
                                     dispatch(UpdateStatusRight i)   
                         ) 
            dispatch WorkIsCompleteRight
        }   

let update (msg: Msg) (m: Model) : Model * Cmd<Msg> = 
    match msg with 
        | UpdateStatusLeft progress  -> { m with ProgressIndicatorLeft = InProgress progress; ProgressBackgroundLeft = Brushes.White }, Cmd.none                      
        | WorkIsCompleteLeft         -> { m with ProgressIndicatorLeft = Idle; ProgressBackgroundLeft = Brushes.LightSkyBlue }, Cmd.none                       
        | UpdateStatusRight progress -> { m with ProgressIndicatorRight = InProgress progress; ProgressBackgroundRight = Brushes.White }, Cmd.none                       
        | WorkIsCompleteRight        -> { m with ProgressIndicatorRight = Idle; ProgressBackgroundRight = Brushes.LightSkyBlue }, Cmd.none 
        | TestButtonLeftEvent        ->
                                      let incrementDelayedCmd (dispatch: Msg -> unit) : unit =  //THIS WORKS
                                          let delayedDispatch = longRunningOperationLeft dispatch                                                      
                                          Async.StartImmediate delayedDispatch
                                      { m with ProgressIndicatorLeft = InProgress 0 }, Cmd.ofSub incrementDelayedCmd           
        | TestButtonRightEvent       ->
                                      let incrementDelayedCmd (dispatch: Msg -> unit) : unit =  //NOT WORKING              
                                          let delayedDispatch = longRunningOperationRight dispatch
                                          Async.StartImmediate delayedDispatch
                                      { m with ProgressIndicatorRight = InProgress 0 }, Cmd.ofSub incrementDelayedCmd   
 
let bindings(): Binding<Model,Msg> list =
   [      
      "ProgressLeftBackg"    |> Binding.oneWay(fun m -> m.ProgressBackgroundLeft) 
      "ProgressRightBackg"   |> Binding.oneWay(fun m -> m.ProgressBackgroundRight) 
      "ProgressLeft"         |> Binding.oneWay(fun m -> match m.ProgressIndicatorLeft with Idle -> 0.0 | InProgress v -> float v)
      "ProgressRight"        |> Binding.oneWay(fun m -> match m.ProgressIndicatorRight with Idle -> 0.0 | InProgress v -> float v)       
      "TestButtonLeft"       |> Binding.cmdIf(TestButtonLeftEvent, fun m -> match m.ProgressIndicatorLeft with Idle -> true | _ -> false)
      "TestButtonRight"      |> Binding.cmdIf(TestButtonRightEvent, fun m -> match m.ProgressIndicatorRight with Idle -> true | _ -> false) 
   ]

即使将“i”索引与进度条值绑定对 MainWindow 中的收集函数有效,也无法解决问题。在现实生活中,用于处理进度条值的集合函数位于主 window 文件“上方”的其他文件中。像这样:

文件:MainLogicRight.fs

//F# code
module MainLogicRight

let textBoxString3 low high path = 

    //some code

    let myArray() =            
        Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)
        |> Option.ofObj
        |> optionToArraySort "..." "..."           
        |> Array.collect
                (fun item -> 
                            let arr = 
                                let p = prefix + "*"
                                Directory.EnumerateDirectories(item, p) 
                                |> Option.ofObj
                                |> optionToArraySort "..." "..."
                                |> Array.Parallel.mapi(fun i item -> 
                                                                let arr = Directory.EnumerateFiles(item, "*.jpg", SearchOption.TopDirectoryOnly)
                                                                          |> Option.ofObj   
                                                                          |> optionToArraySort "..." "..."                                                  
                                                                arr.Length
                                                      ) 
                            arr                    
                )    

我知道(可能)无法将 pb 值与非索引函数绑定,例如 Array.collect但重要的是 - 如何将 pb 值与 List/Array 中的“i”索引绑定。mapi/iteri ( Array.Parallel.mapi 在这种情况下) ?

编辑3:

根据Bent 的最后回答,删除了我现在无关的文本和评论。 基于答案的示例是 here.

此答案解释了在 Elmish.WPF 中如何从异步完成对用户界面的进度更新。

我创建了一个演示此功能的 example on GitHub。该示例还演示了另一种调用异步函数和接收结果的方法。它还演示了如何使用 mkProgram 而不是 mkSimple。该演示可用作您的 Elmish.WPF 应用程序的起始模板。

演示中的这段代码显示了从异步更新用户界面所涉及的基本代码。

这两种技术都基于 Elmish Book 中的代码。您会在那里找到很多代码,这些代码在 Elmish.WPF.

中也很有用

我在这里没有尝试更新进度条,只更新了状态文本框,但是从这里您可以很容易地弄清楚如何更新任何内容。

| UpdateStatusText statusText ->
    { m with StatusText = statusText }, Cmd.none
| RunWithProgress ->
    let incrementDelayedCmd (dispatch: Msg -> unit) : unit =
        let delayedDispatch = async {
            do! Async.Sleep 1000
            dispatch (UpdateStatusText "One")
            do! Async.Sleep 1000
            dispatch (UpdateStatusText "Two")
            do! Async.Sleep 1000
            dispatch (UpdateStatusText "Three")
            }
        Async.StartImmediate delayedDispatch
    { m with StatusText = "Started progress." }, Cmd.ofSub incrementDelayedCmd

更新:

我现在已经在 GitHub 上更新了演示项目,以便它演示来自异步的进度条(和状态文本)的更新。这些是基本部分的片段。

从异步发送的两条消息的声明。

| UpdateStatus of statusText:string * progress:int
| WorkIsComplete // This message could carry a result from the work done.

两条消息的处理。

| UpdateStatus (statusText, progress) ->
    { m with StatusText = statusText; Progress = progress }, Cmd.none
| WorkIsComplete ->
    { m with StatusText = "Work was completed."; Progress = 0 }, Cmd.none
| RunWithProgress ->
    let incrementDelayedCmd (dispatch: Msg -> unit) : unit =
        let delayedDispatch = async {
            do! Async.Sleep 1000
            dispatch (UpdateStatus ("Early work", 30))
            do! Async.Sleep 1000
            dispatch (UpdateStatus ("Still working", 60))
            do! Async.Sleep 1000
            dispatch (UpdateStatus ("Late work", 90))
            do! Async.Sleep 1000
            dispatch WorkIsComplete
            }
        Async.StartImmediate delayedDispatch
    { m with StatusText = "Started progress." }, Cmd.ofSub incrementDelayedCmd

字段 Progress 声明为 int。

    Progress: int

ProgressBar 的 属性 值为浮点数,因此需要在绑定中转换为浮点数。

"Progress" |> Binding.oneWay (fun m -> float m.Progress)

当然我们可以在模型中将 Progress 声明为浮点数,但我想借此机会指出模型不必与组件属性的数据类型对齐。我们当然可以在绑定中以任何我们想要的方式映射。

关于调度程序的最后一点说明。这可以通过 Cmd.ofSub 访问,也可以通过 WkProgram.Subscribe 访问。也许在其他情况下会详细介绍这一点,但现在请注意:使用调度程序发送消息是线程安全的。这意味着您也可以从顶级异步函数中 运行 的异步函数向模型发送进度消息(或任何消息),或者例如来自计时器事件,或任何地方。

最终更新:GitHub 上的演示现在比此处显示的稍微高级一些,但原理仍然相同,因此我不会费心更新此答案中的源代码。任何对此感兴趣的人很可能无论如何都需要完整的演示源,除非你已经很了解 Elmish.WPF


问题的最后一部分,后来补充的,在这里回答。

当进行冗长的and/or CPU 密集型工作时,应按照下面的longRunningOperationLeft 功能所示进行操作。这也显示了其他地方的功能,不应该依赖于 GUI,可以以这样一种方式编写,即进度更新可以发送到 GUI。

下面显示的 longRunningOperationRight 做错了,阻塞了 GUI。

我在异步和任务方面的专业知识不是很好,但我认为从 Elmish 调用的顶级异步函数(例如 longRunningOperationLeft)与 运行在同一个线程上Elmish 循环,这就是为什么它们不应该被任何冗长或 CPU 密集的东西阻塞的原因。相反,这种阻塞工作需要进入子计算(例如 workToDo)。 longRunningOperationLeft的作用是等待工作,而不是自己做工作,以免阻塞GUI。

不知道List.mapi里面能不能有异步操作。我怀疑不是。无论如何,我怀疑您的真实案例不需要它。

Mira 更新:你是对的。在我的真实案例中不需要。在 List/array.mapi 中添加 reportProgress i(就像在您的代码中一样)就足够了。

let private lengthyWork () =
    [1..20_000_000] |> List.reduce ( * ) |> ignore

let private workToDo reportProgress = async {
    reportProgress 0
    lengthyWork ()
    reportProgress 25
    lengthyWork ()
    reportProgress 50
    lengthyWork ()
    reportProgress 75
    lengthyWork ()
    reportProgress 100
    return 7
    }

// This is good.
let private longRunningOperationLeft dispatch = async {
    let reportProgress progress = dispatch (UpdateStatusLeft progress)
    let! hardWork = Async.StartChild (workToDo reportProgress)
    do! Async.Sleep 1000 // Can do some async work here too, while waiting for hardWork to finish.
    let! result = hardWork
    dispatch WorkIsCompleteLeft
    }

// This is not good. Blocking GUI.
let private longRunningOperationRight dispatch = async {
    dispatch (UpdateStatusRight 0)
    lengthyWork ()
    dispatch (UpdateStatusRight 25)
    lengthyWork ()
    dispatch (UpdateStatusRight 50)
    lengthyWork ()
    dispatch (UpdateStatusRight 75)
    lengthyWork ()
    dispatch (UpdateStatusRight 100)
    dispatch WorkIsCompleteRight
    }