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.
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.
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.
Handling of the two messages.
The field Progress is declared as an int.
The property Value of ProgressBar is a float, so a cast to float is needed in the binding.
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 asworkToDo
). The role oflongRunningOperationLeft
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.