0

I've a ListBox fed by a view model property (Photos) which is an ObservableCollection of objects containing paths to image files. The ListBox displays the images, which can be numerous:

View:

<ListBox ItemsSource="{Binding Path=Photos}"
         SelectionChanged="PhotoBox_SelectionChanged">
    ...
</ListBox>

Code behind (which could be improved to run asynchronously...):

void RefreshPhotoCollection (string path) {
    Photos.Clear ();
    var info = new DirectoryInfo (path);
    try {
        foreach (var fileInfo in info.EnumerateFiles ()) {
            if (FileFilters.Contains (fileInfo.Extension)) {
                Photos.Add (new Photo (fileInfo.FullName));
            }
        }
    }
    catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) {
        ...
    }
}

I've managed to display a wait cursor when running RefreshPhotoCollection by using this method which involves a IDisposable:

using (this.BusyStackLifeTracker.GetNewLifeTracker())
    {
        // long job
    }

But the cursor is reset to a pointer at the end of this method, when the view is notified of the collection change. The ListBox then renders itself, but the wait cursor is not displayed while this is done. There is were I have a problem.

How can I manage to have the ListBox displaying a wait cursor until the update is complete?

mins
  • 6,478
  • 12
  • 56
  • 75

2 Answers2

2

Start by creating a "busy" flag in your view model:

private bool _Busy = false;
public bool Busy
{
    get { return this._Busy; }
    set
    {
        if (this._Busy != value)
        {
            this._Busy = value;
            RaisePropertyChanged(() => this.Busy);
        }
    }
}

Then move all your work into a Task, which sets this busy flag and resets it afterwards (note how you'll have to invoke the GUI thread's dispatcher whenever you add the photo to the collection itself):

private void DoSomeWork()
{
    Task.Run(WorkerProc);
}

private async Task WorkerProc()
{
    this.Busy = true;
    for (int i = 0; i < 100; i++)
    {
        // simulate loading a photo in 500ms
        var photo = i.ToString();
        await Task.Delay(TimeSpan.FromMilliseconds(500));

        // add it to the main collection
        Application.Current.Dispatcher.Invoke(() => this.Photos.Add(photo));
    }
    this.Busy = false;
}

And then in your XAML give your MainWindow a style that sets the cursor whenever this flag is set:

<Window.Style>
    <Style TargetType="{x:Type Window}">
        <Style.Triggers>
            <DataTrigger Binding="{Binding Busy}" Value="True">
                <Setter Property="Cursor" Value="Wait" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Window.Style>

UPDATE: Placing the work in a Task means you won't be starving the GUI thread, so the ListBox will be updating as you go along rather than waiting until you've finished loading all the photos. And if it's the ListBox update itself that's taking too long then that's what you should be trying to address, e.g. look into whether any converters are running slow or whether your Photo data structure needs to be provide to the view in a more optimal format. I think you'll find though that simply enabling virtualization will probably go a long way towards improving your front-end responsiveness:

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
        <VirtualizingStackPanel />
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>

Unfortunately virtualization is something that's very easy to break though, so you'll want to make sure it's working by checking your Live Visual Tree while your app is running to make sure that ListBoxItems are only being created for the items that are actually visible on screen.

Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
  • Thanks Mark, but I've already done that (except using a task to build the collection) and even prevented ObservableCollection to notify the view after each single update. However this makes the wait cursor being shown while the collection is built, not when the ListBox updates itself with the new collection content. – mins Nov 26 '19 at 12:55
2

This pattern does these things:

  1. Places the cursor change logic in a flag variable for other uses.
  2. Provides a safe way for other threads to change GUI items on the gui thread.
  3. Adds the photos in a separate thread which will allow for UI to be responsive.
  4. Actual photo added into the collection is done on the Gui thread for safety.
  5. Wait cursor won't jump/change state as photos are added.
  6. Checks for a minimum waited processing time to give the appearance of doing something. That wait time can be changed as needed. If the minimum wait time is not hit, no extra time is done.

Notification on VM This allows any non gui thread to do a safe gui operation.

public static void SafeOperationToGuiThread(Action operation)
{
    System.Windows.Application.Current?.Dispatcher?.Invoke(operation);
}

Status Flag Then provide an Ongoing Flag which the view can be re-used if needed, which also sets the cursor:

private bool _IsOperationOnGoing;

public bool IsOperationOnGoing
{
    get { return _IsOperationOnGoing; }
    set {
        if (_IsOperationOnGoing != value)
        {
            _IsOperationOnGoing = value;

            SafeOperationToGuiThread(() => Mouse.OverrideCursor = (_IsOperationOnGoing) ? Cursors.Wait : null  );
            OnPropertyChanged(nameof(IsOperationOnGoing));
        }
    }
}

Add photos in Seperate Thread Then in your photo add, do this pattern where the gui thread turns off the cursor, then a separate task/thread loads the photos. Also the time is monitored for a viable consistent wait time cursor to be shown:

private Task RunningTask { get; set; }

void RefreshPhotoCollection (string path) 
{
   IsOperationOnGoing = true;
   RunningTask = Task.Run(() =>
     {
         try { 
              TimeIt( () =>
                          {
                          ... // Enumerate photos like before, but add the safe invoke:

                          SafeOperationToGuiThread(() => Photos.Add (new Photo (fileInfo.FullName););
                          },
                         4   // Must run for 4 seconds to show the user.
                    );
              }
         catch() {}
         finally 
             {
               SafeOperationToGuiThread(() => IsOperationOnGoing = false);
             }
          });
     }

}

Wait time Check

How can I manage to maintain the wait cursor until after the view ListBox update is complete so that the user really knows when the UI is responsive again?

Here is the waiting operation method which will fill in the time to wait if the minimum operation time is not hit:

public static void TimeIt(Action work, int secondsToDisplay = 2)
{
    var sw = Stopwatch.StartNew();
    
    work.Invoke();

    sw.Stop();

    if (sw.Elapsed.Seconds < secondsToDisplay)
        Thread.Sleep((secondsToDisplay - sw.Elapsed.Seconds) * 1000);    
}
  

This can be modified to use an async call if needed with minimal changes.


Alternate to @mins tag line in profile:

 Light a man a fire and he is warm for a day. 
 Set a man a fire and he is warm for the rest of his life.
Community
  • 1
  • 1
ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
  • Thanks for the time and the tag line suggestion :) I'll update my question to better explain, as I think I'm misunderstood: I'm looking for a way to show the wait cursor after the collection has been updated, that is during the period between when the ListBox is notified of the collection change, and the new items have all been rendered (I cannot delay the pointer cursor restoration from the code). That said your current code is still helpful to confirm the solution I have used is valid. – mins Nov 26 '19 at 13:29
  • If in my code you set the total wait time to 5 seconds and the process takes 3 seconds, the code will wait another 2 seconds to cover. Which would give the appearance of waiting past the loading phase. Otherwise you need to hook up to a event on the `Loaded` event of the `ListBox` to possibly turn off a wait cursor. – ΩmegaMan Nov 26 '19 at 15:06
  • My code is designed for a minimum wait time operations. 5 seconds of wait cursor gives the impression of a true wait. If the operation goes over 5, no wait is done. – ΩmegaMan Nov 26 '19 at 15:13