Incrementing progress bar on dock panel from external class function

381
2
08-17-2022 07:50 AM
LewisTrotter
New Contributor III

Hello all,

I've been trying to implement a progress bar on a dock pane that increments as a function in a separate class downloads a list of GeoTiffs using HttpClient.

I've successfully got this to work using the approach provided in the code sample Overlay3D.

However, unlike this example, my list of tasks I want to increment occurs in a separate function outside of the dock pane, and I'm not sure how to pass the information from that function back to the dock pane.

Any advice is welcome. I have provided a cut down code example below of my current setup, the comments should show where I am getting stuck:

 

XAML progress bar, with two bindings (MaxProgressValue and Progress Value) back to dock pane view model.

 

 

<!-- Progress bar-->
<ProgressBar Name="PrgProgressBar" 
             Grid.Column="0"
             Margin="0,0,5,0"  
             Height = "10"
             HorizontalAlignment="Stretch"
             Minimum="1"
             Maximum="{Binding Path=MaxProgressValue, Mode=OneWay}"
             Value="{Binding Path=ProgressValue, Mode=OneWay}"  
             IsIndeterminate="False">
</ProgressBar>

 

 

ViewModel related to progress bar, with a button to call a function to download geotiffs.

 

 

// Getter, Setter for progress bar
private double _maxProgressValue = 100;
public double MaxProgressValue
{
  get { return _maxProgressValue; }
  set { SetProperty(ref _maxProgressValue, value, () => MaxProgressValue); }
}

// Update progress bar method (based on Overlay3D)
private int _iProgressValue = -1;
private int _iMaxProgressValue = -1;
private void ProgressUpdate(int iProgressValue, int iMaxProgressValue)
{
  if (System.Windows.Application.Current.Dispatcher.CheckAccess())
  {
    if (_iMaxProgressValue != iMaxProgressValue)
    {
      MaxProgressValue = iMaxProgressValue;
    }

    if (_iProgressValue != iProgressValue)
    {
      ProgressValue = iProgressValue;
    }
  }
  else
  {
    ProApp.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)(() => {
    if (_iMaxProgressValue != iMaxProgressValue)
    {
      MaxProgressValue = iMaxProgressValue;
    }
    if (_iProgressValue != iProgressValue)
    {
      ProgressValue = iProgressValue;
    }
    }));
  }
}

// Button on dock panel to run download function
public ICommand CmdRun
{
  get
    {
      return new RelayCommand(async () =>
        {
          // Excluded various things
          // ...
 
          // Initialise data structure STAC that contains urls and functions
          Stac stac = new Stac(...)

          // Reset progressor value and maximum value
          ProgressUpdate(1, stac.Result.Features.Count);

          // Start queued task and download geotifs
          await QueuedTask.Run(() => 
          {
            // Begin download tifs loop
            // How do I monitor progession? 
            // Pass a progressor variable via IProgress?
            await stac.DownloadFeaturesAsync(assets, outputFolder);
          });
        }
      }

 

 

 Stac class for DownloadFeatureAsync.

 

 

public async Task DownloadFeaturesAsync(List<string> assetNames, string outputFolder)
{
  // Some uninteresting work...
  // ...
  
  // Iterate dates in STAC query and download tif to folder
  foreach (string date in dates)
  {
    // Open a HTTP client
    var client = new HttpClient();

    // Create a list of downloader tasks
    int i = 0; // test
    var tasks = new List<Task>();
    foreach (var item in items)
    {
      string date = item.Key;
      string url = item.Value[0];
      string filename = item.Value[1];

      tasks.Add(Task.Run(async () =>
      {
        // Do some things...

        // Query URL and ensure success
        var response = await client.GetAsync(url);
        response.EnsureSuccessStatusCode();

        // Download file to output folder
        using (FileStream fs = new FileStream(filepath, FileMode.CreateNew))
        {
          await response.Content.CopyToAsync(fs);
        }

        // Increment progress bar... how...?!
        // ...
      }));
    }

  // Run all download tasks
  await Task.WhenAll(tasks);
}

 

 

0 Kudos
2 Replies
Wolf
by Esri Regular Contributor
Esri Regular Contributor

You can define a reference to your view model in your Module.  Since the Module class is a singleton you can then use that reference from your external class.  Look at this example:  arcgis-pro-sdk-community-samples/Map-Exploration/MapToolIdentifyWithDockpane at master · Esri/arcgis...

in this sample the reference to the view modal is defined in the Module class as follows:

internal static MapToolIdentifyDockpaneViewModel MapToolIdentifyDockpaneVM { get; set; }

In the Ctor of the view model that 'global accessible' reference is initialized:

protected MapToolIdentifyDockpaneViewModel()
{
  // register self with Module class
  Module1.MapToolIdentifyDockpaneVM = this;
}

And it is then used in an external class (the tool in this case) here (https://github.com/Esri/arcgis-pro-sdk-community-samples/tree/master/Framework/DockpaneSimple 😞

protected override async Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
  Module1.MapToolIdentifyDockpaneVM.ClearListOfFeatures();
  await QueuedTask.Run(() =>
  {
    Module1.MapToolIdentifyDockpaneVM.AddToListOfFeatures($@"Layer: {featLyr.Name} obj id: {feat.GetObjectID()} Display Expression: {displayExp}");
  });
  return true;
}

If you call your progressbar updates from a background thread, you might want to consider the following method that ensure execution on the UI thread (sample: arcgis-pro-sdk-community-samples/Map-Exploration/MapToolIdentifyWithDockpane at master · Esri/arcgis...😞

/// <summary>
/// utility function to enable an action to run on the UI thread (if not already)
/// </summary>
/// <param name="action">the action to execute</param>
/// <returns></returns>
internal static Task RunOnUIThread(Action action)
{
    if (OnUIThread)
    {
        action();
        return Task.FromResult(0);
    }
    else
        return Task.Factory.StartNew(action, System.Threading.CancellationToken.None, TaskCreationOptions.None, QueuedTask.UIScheduler);
}

/// <summary>
/// determines if the application is currently on the UI thread
/// </summary>
private static bool OnUIThread
{
    get
    {
        if (FrameworkApplication.TestMode)
            return QueuedTask.OnWorker;
        else
            return System.Windows.Application.Current.Dispatcher.CheckAccess();
    }
}

 

LewisTrotter
New Contributor III

Thanks Wolf,

I ended up using IProgress to increment my progress bar. Basically, I associate my ProgressValue (which is bound to UI) with IProgress, and then just use progress.Report(i) within my button RelayCommand logic to increment it. Seems to work perfectly.

Do you think this is a valid approach?

0 Kudos