Best way to show progress?

1935
3
Jump to solution
04-22-2020 03:49 AM
Vidar
by
Occasional Contributor II

I've created a dockpane that adds layers to a new map pane - this can be a long running process.

I think a progressbar would be nice for the user to see how things are going.

What is the ESRI way or best way of doing things like this? Put a ProgressBar control on the dockpane? Or have a ProgressDiaglog popup?

I see no concrete examples using the standard WPF ProgressBar control on ESRI GitHub, only the ProgressDialog - which is the main reason I am asking the question.

0 Kudos
1 Solution

Accepted Solutions
Wolf
by Esri Regular Contributor
Esri Regular Contributor

Regarding your question:

What is the ESRI way or best way of doing things like this? Put a ProgressBar control on the dockpane? Or have a ProgressDiaglog popup?

I guess this depends on the workflow that your add-in implements: if your workflow depends on the layers that you're loading i would recommend to use the ProgressDialog popup - since you have to wait for those layers loading before you do any further work.  If you just load those layers for reference and the layers are not required (optional) for further work you can use a WPF ProgressBar on your dockpane.

Regarding your question:

I see no concrete examples using the standard WPF ProgressBar control on ESRI GitHub, only the ProgressDialog - which is the main reason I am asking the question.

The example you are looking for is here:

  Map-Exploration - Overlay 3D

Since you likely update the UI from a background thread (i.e. QueuedTask.Run) make sure that you properly dispatch your progress update to the UI thread as shown here:  arcgis-pro-sdk-community-samples/Overlay3DDockpaneViewModel.cs at 6d7d39d6169b59d34be63e415cdd88d561... 

private void ProgressUpdate(string sText, int iProgressValue, int iProgressMax)
    {
      if (System.Windows.Application.Current.Dispatcher.CheckAccess())
      {
        if (_iProgressMax != iProgressMax) MaxProgressValue = iProgressMax;
        else if (_iProgressValue != iProgressValue)
        {
          ProgressValue = iProgressValue;
          ProgressText = (iProgressValue == iProgressMax) ? "Done" : $@"{(iProgressValue * 100 / iProgressMax):0}%";
        }
        if (sText != _previousText) UpdateStatus = sText;
        _previousText = sText;
        _iProgressValue = iProgressValue;
        _iProgressMax = iProgressMax;
      }
      else
      {
        ProApp.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
          (Action)(() =>
          {
            if (_iProgressMax != iProgressMax) MaxProgressValue = iProgressMax;
            else if (_iProgressValue != iProgressValue)
            {
              ProgressValue = iProgressValue;
              ProgressText = (iProgressValue == iProgressMax) ? "Done" : $@"{(iProgressValue * 100 / iProgressMax):0}%";
            }
            if (sText != _previousText) UpdateStatus = sText;
            _previousText = sText;
            _iProgressValue = iProgressValue;
            _iProgressMax = iProgressMax;
          }));
      }
    }

And finally if you want to show progress in WPF and you don't have proper percentage completion feedback (0-100%) in your load routines you can also use the 'CircularAnimationControl' as shown in step 15 of this example:

arcgis-pro-sdk-community-samples/Framework/DockPaneBookmarkAdvanced at f5f9dda18efa173a56d128f3ea64c... 

View solution in original post

3 Replies
Wolf
by Esri Regular Contributor
Esri Regular Contributor

Regarding your question:

What is the ESRI way or best way of doing things like this? Put a ProgressBar control on the dockpane? Or have a ProgressDiaglog popup?

I guess this depends on the workflow that your add-in implements: if your workflow depends on the layers that you're loading i would recommend to use the ProgressDialog popup - since you have to wait for those layers loading before you do any further work.  If you just load those layers for reference and the layers are not required (optional) for further work you can use a WPF ProgressBar on your dockpane.

Regarding your question:

I see no concrete examples using the standard WPF ProgressBar control on ESRI GitHub, only the ProgressDialog - which is the main reason I am asking the question.

The example you are looking for is here:

  Map-Exploration - Overlay 3D

Since you likely update the UI from a background thread (i.e. QueuedTask.Run) make sure that you properly dispatch your progress update to the UI thread as shown here:  arcgis-pro-sdk-community-samples/Overlay3DDockpaneViewModel.cs at 6d7d39d6169b59d34be63e415cdd88d561... 

private void ProgressUpdate(string sText, int iProgressValue, int iProgressMax)
    {
      if (System.Windows.Application.Current.Dispatcher.CheckAccess())
      {
        if (_iProgressMax != iProgressMax) MaxProgressValue = iProgressMax;
        else if (_iProgressValue != iProgressValue)
        {
          ProgressValue = iProgressValue;
          ProgressText = (iProgressValue == iProgressMax) ? "Done" : $@"{(iProgressValue * 100 / iProgressMax):0}%";
        }
        if (sText != _previousText) UpdateStatus = sText;
        _previousText = sText;
        _iProgressValue = iProgressValue;
        _iProgressMax = iProgressMax;
      }
      else
      {
        ProApp.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background,
          (Action)(() =>
          {
            if (_iProgressMax != iProgressMax) MaxProgressValue = iProgressMax;
            else if (_iProgressValue != iProgressValue)
            {
              ProgressValue = iProgressValue;
              ProgressText = (iProgressValue == iProgressMax) ? "Done" : $@"{(iProgressValue * 100 / iProgressMax):0}%";
            }
            if (sText != _previousText) UpdateStatus = sText;
            _previousText = sText;
            _iProgressValue = iProgressValue;
            _iProgressMax = iProgressMax;
          }));
      }
    }

And finally if you want to show progress in WPF and you don't have proper percentage completion feedback (0-100%) in your load routines you can also use the 'CircularAnimationControl' as shown in step 15 of this example:

arcgis-pro-sdk-community-samples/Framework/DockPaneBookmarkAdvanced at f5f9dda18efa173a56d128f3ea64c... 

Vidar
by
Occasional Contributor II

I'll be honest - it kind of blows my mind what's going on here!

0 Kudos
CharlesMacleod
Esri Regular Contributor

Basically, if you ignore the logic Wolf has implemented to check whether to change progress value and text or not, the pattern Wolf is using (in WPF) is the same standard pattern as with old school WinForms for updating the UI from a non-UI thread. Namely:

Check if you are on the UI thread, if so, update progress directly. If not, invoke the update on the UI thread first.

So the pattern resolves to:

private void UpdateProgress(...) {
   //whatever "update progress" means - change a counter, text, etc


  NotifyPropertyChanged("ProgressValue");//Assuming something is bound to this
}

private void UpdateProgressSafely(...) {
  //FrameworkApplication and System.Windows.Application are the same thing
  if (FrameworkApplication.Current.Dispatcher.CheckAccess())
    //On the UI - do update directly
    UpdateProgress(...);
  else {
    //We are on a background thread, invoke on the UI
    FrameworkApplication.Current.Dispatcher.BeginInvoke((Action)(() => UpdateProgress(...)));
  
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Which is pretty much copy/paste ready. UpdateProgressSafely() can be called from any thread. The last thing probably worth some explanation is this intuitive piece of syntax:

//what is all that "(Action)(() => .....) for?
FrameworkApplication.Current.Dispatcher.BeginInvoke((Action)(() => UpdateProgress(...)));‍‍‍‍‍‍‍‍

Well, let's remove all that "(Action)(.....)" cast reducing the syntax to a more legible:

FrameworkApplication.Current.Dispatcher.BeginInvoke(() => UpdateProgress(...));

where "() => ..." is the standard anonymous delegate usage. However, intellisense will complain that:

It "Cannot convert lambda expression to type 'Delegate' because it is not a delegate type" and you will get a compiler error. BeginInvoke has numerous overloads and without the "Action" cast the compiler cannot resolve which overload we want it to use. We have to help it by providing an explicit "Action" that wraps the delegate "() => ..." bringing us back full circle to "BeginInvoke( (Action) (() => ....) )".

To simplify the syntax, you can also add your own utility method like this in your Module that can be used to call any method safely on the UI:

//in your module or some helper class
public void RunOnUIThread(Action action) {
   if (FrameworkApplication.Current.Dispatcher.CheckAccess())
     action();
   else
     FrameworkApplication.Current.Dispatcher.BeginInvoke(action);
}

//usage
 await QueuedTask.Run(() => {
    //some length process
    //...
    Module1.Current.RunOnUIThread(() => UpdateProgess(50, "working..."));