avaloniauiavalonia

ProgressBar doesn't update when changing value in OnLoaded method


I have a window that shows when another process is completing. The other process sets a boolean flag when done - a function returning the status of this flag is passed to the window (this is the function referred to by _signalCompletedFunction). The window 'winWait' just shows a cycling progress bar (moves from 1 to 10 repeatedly while waiting). The problem is that the progress bar never updates. (Similar code works in WinForms where 'OnLoaded' is replaced by WinForms 'OnActivated', but the WinForms progress bar 'Refresh' method is also required - this method doesn't exist in Avalonia).

public partial class winWait : Window
{
    private readonly Func<bool> _signalCompletedFunction;
    
    public winWait()
    {
        InitializeComponent();
    }

    public winWait(Func<bool> signalCompletedFunction) : this()
    {
        _signalCompletedFunction = signalCompletedFunction;

        this.prgWait.Minimum = 0;
        this.prgWait.Maximum = 10;
    }

    protected override void OnLoaded(RoutedEventArgs e)
    {
        base.OnLoaded(e);

        while (!_signalCompletedFunction())
        {
            double mod = (this.prgWait.Value + 1) % this.prgWait.Maximum;
            this.prgWait.Value = mod == 0 ? this.prgWait.Maximum : mod;
        }

        this.Close();
    }
}

Solution

  • The synchronous while loop blocks the UI thread, so that no UI updates are made - which is what the WinForms Refresh method does.

    Replace the loop by a timer implementation like this:

    protected override void OnLoaded(RoutedEventArgs e)
    {
        base.OnLoaded(e);
    
        var timer = new DispatcherTimer
        {
            Interval = TimeSpan.FromMilliseconds(50)
        };
    
        timer.Tick += (s, e) =>
        {
            if (_signalCompletedFunction())
            {
                timer.Stop();
                Close();
            }
            else
            {
                prgWait.Value = ((prgWait.Value + 1) % (prgWait.Maximum + 1)) + prgWait.Minimum;
            }
        };
    
        timer.Start();
    }
    

    Besides that, "restarting" a ProgressBar is an atypical user experience. Better set the ProgressBar to inderminate, as shown below. The update behavior of the ProgressBar would then also be independent of the timer interval, because the indeterminate state is animated.

    protected override void OnLoaded(RoutedEventArgs e)
    {
        base.OnLoaded(e);
    
        prgWait.IsIndeterminate = true;
    
        var timer = new DispatcherTimer
        {
            Interval = TimeSpan.FromSeconds(1)
        };
    
        timer.Tick += (s, e) =>
        {
            if (_signalCompletedFunction())
            {
                timer.Stop();
                Close();
            }
        };
    
        timer.Start();
    }
    

    Instead of a timer you may alternatively implement an asynchronous wait loop. It runs in a Loaded event handler, because only event handlers methods should be declared async void (other async methods should be async Task).

    With an anonymous handler method you could then even save the _signalCompletedFunction field.

    public winWait(Func<bool> signalCompletedFunction)
    {
        InitializeComponent();
    
        prgWait.IsIndeterminate = true;
    
        Loaded += async (s, e) =>
        {
            while (!signalCompletedFunction())
            {
                await Task.Delay(1000);
            }
    
            Close();
        };
    }
    

    Finally, it is questionably wether you need to pass a Func to the Window at all. You may perhaps simply create and open the Window, let the indeterminate ProgressBar run, and close the Window as soon as the application has reached the completed state. It seems redundant that the Window itself performs the state check.