Updating ProgressBar.Value in FsXaml and ElmishWPF

299 Views Asked by At

I am trying to update ProgressBar.Value in FsXaml. In C#, I used the below-mentioned code. I haven't tried to implement the C# approach in F# as using a public field (myCaller) does not seem to me as being a functional approach (let alone the fact that I do not know if it is at all possible to use this C# approach in F#).

//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
        }
    }

My question is: What is the best (or at least good) functional approach in F# (FsXaml/code behind) for updating ProgressBar.Value?

EDIT1:

Irrelevant code and text deleted. Those not interested in Elmish.WPF please wait until an answer related to FsXaml appears.

EDIT2:

Elmish.WPF

I tried to deal with the ProgressBar issue using Bent Tranberg's comments & answer and his excellent example code. My adaptation works for a for-loop, but not for List.map(i)/iter(i), which are collection functions I actually need the progress bar for. Here is the simplified code:

File: 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) 
   ]

Even if binding the "i" index with the progress bar value had worked for collection functions in the MainWindow, it won't solve the problem. In a real life situation, the collection functions intended to work with the progress bar value are in other files "above" the main window file. Like this:

file: 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                    
                )    

I understand that it is (probably) not possible to bind the pb value with a non-indexed function such as Array.collect. But what is important - how to bind the pb value with the "i" index in List/Array.mapi/iteri (Array.Parallel.mapi in this case) ?

EDIT3:

Based on the last answer by Bent, the now-irrelevant texts and comments of mine were deleted. An example based on the answers is here.

1

There are 1 best solutions below

13
On BEST ANSWER

This answer explains how, in Elmish.WPF, progress updates to the user interface can be done from an async.

I have created an example on GitHub that demoes this. The example also demoes another way to call async functions and receive results. And it also demoes how to use mkProgram instead of mkSimple. The demo can be used as a starting template for your Elmish.WPF applications.

This snippet from the demo show the essential code involved in updating a user interface from an async.

Both techniques are based on code from the Elmish Book. You will find a lot of code there that is useful also in Elmish.WPF.

I haven't tried to update a progress bar here, only a status text box, but from this you'll very easily figure out what to do to update anything.

| 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

UPDATE:

I have now updated the demo project on GitHub so that it demoes updates of a progress bar (and status text) from the async. These are snippets of the essential pieces.

Declaration of the two messages dispatched from the async.

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

Handling of the two messages.

| 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

The field Progress is declared as an int.

    Progress: int

The property Value of ProgressBar is a float, so a cast to float is needed in the binding.

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

Of course we can declare Progress in the model as a float, but I wanted to take this opportunity to point out that the model doesn't have to align with the data types of the properties of the components. We can of course map in whatever way we want in the bindings.

One final note on the dispatcher. This is accessible through Cmd.ofSub, and also through WkProgram.Subscribe. More about that on another occasion maybe, but note this now: Sending messages with the dispatcher is thread safe. This means you can send progress messages (or any message) to the model also from async functions that run within your top level async function, or e.g. from a timer event, or anywhere really.

FINAL UPDATE : The demo on GitHub is now slightly more advanced than shown here, but the principle is still the same, so I won't bother to update the source in this answer. Anybody interested in this will most probably need the complete demo source anyway, unless you're already well into Elmish.WPF


The last part of the question, added later, is answered here.

When doing lengthy and/or CPU-intensive work, then this should be done as shown in the longRunningOperationLeft function below. This also shows how functions elsewhere, that should not be dependent on the GUI, can be written in such a way that progress updates can be sent to the GUI.

The longRunningOperationRight shown below is doing it the wrong way, blocking the GUI.

My expertise on async and task stuff is not very good, but I think the top-level async functions (such as longRunningOperationLeft) called from Elmish are running on the same thread as the Elmish loop, and this is why they should not be blocked with anything lengthy or CPU-intensive. Instead, that kind of blocking work needs to go into a child computation (such as workToDo). The role of longRunningOperationLeft is to await work, but not do work itself, lest it blocks the GUI.

I don't know whether List.mapi can have an async operation inside it. I suspect not. Anyhow, I suspect that won't be needed for your real-life case.

UPDATE by Mira: You are right. Not needed in my real-life case. Adding reportProgress i (like in your code) inside List/array.mapi is enough.

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
    }