I have a scenario where I need to cycle through some tiles of the map and export them on tif and pdf files. The code I've written to achieve the task is ok and it works fine. Since the operation can be very long, depending on the number of source items, I'd like to show a cancelable progress window to the user so that he can stop the job if needed.
The window appears, but the Export operation prevents message, status and CancellationToken from updating. The result is a window which can only show the initial status and message, and cannot be canceled (you can press the button, and ironically it updates the window with the cancel message, but the property progressorSource.Progressor.CancellationToken.IsCancellationRequested is always false).
To show this, I managed to strip down some code about PDF export to fit it inside a single, simplified function:
private async Task ExportTilesAsync(Layout layout, string whereClause)
{
string outputFolder = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "SOME_OUTPUT_FOLDER");
var ctrMapFrame = layout.FindElement("MAPPA") as MapFrame;
var ctrMap = ctrMapFrame.Map;
var progressorSource = new CancelableProgressorSource("Processing...", "Cancelling...");
await QueuedTask.Run(() =>
{
//Get Layer
var layer = ctrMap.FindLayers("qu05_pl").OfType<FeatureLayer>().FirstOrDefault();
//Query layer
FeatureClass pFeatClass = layer.GetFeatureClass();
var nCount = pFeatClass.GetCount();
progressorSource.Progressor.Max = (uint)nCount;
QueryFilter queryFilter = new QueryFilter() { WhereClause = whereClause };
using (RowCursor rowCursor = layer.Search(queryFilter))
{
while (rowCursor.MoveNext())
{
if (progressorSource.Progressor.CancellationToken.IsCancellationRequested)
return;
var currentFeature = rowCursor.Current as Feature;
var idTile = currentFeature.GetFieldValue("ELEMENTO").ToString();
progressorSource.Progressor.Value += 1;
progressorSource.Progressor.Status = $"Processing Tile {idTile}...";
ClipAndRefreshMap(currentFeature, ctrMapFrame);
PDFFormat PDF = new PDFFormat();
PDF.Resolution = 400;
PDF.OutputFileName = Path.Combine(outputFolder, $"{idTile}");
PDF.ImageCompression = ImageCompression.LZW;
PDF.DoEmbedFonts = true;
PDF.DoCompressVectorGraphics = true;
PDF.DoClipToGraphicExtent = false;
//Export active mapView view
if (PDF.ValidateOutputFilePath())
{
layout.Export(PDF);
}
}
}
}, progressorSource.Progressor);
}
ClipAndRefreshMap is the only external function called here, it sets extent and view of the MapFrame in the right way for the current tile (feature) to export. To keep it as short as possible (and since it does not appear to be the problem) I'm omitting the code inside this function, but I can provide it if needed.
If I just remove the call to layout.Export from this code (you can add a Task.Delay() call to make it slower, if needed), everything works: I get message and status update at every cycle and, if I cancel the job, the code intercepts it properly and exits.
I've tried to mess around with threads and priorities but never really found anything useful: all I obtained is to enqueue all the actual exports after the cycle is done, which is obviously not useful. As far as I understand the way I set up the CancelableProgressorSource is ok, I guess it would be useful to have an ExportAsync() method, but it seems like only the synchronous method is available. Is there something about Export operation that prevents it to work properly? How am I supposed to implement my scenario?
Any suggestion is appreciated, thanks.
Hi @Asimov
Have you tried your code by using the CancelableProgressorSource constructor that accepts a progress dialog? Something like this:
var pd = new ArcGIS.Desktop.Framework.Threading.Tasks.ProgressDialog(
"Processing...", "Cancelling...");
var progressorSource = new CancelableProgressorSource(pd);
Using your code sample (modified to use the progress dialog), I got the message to update with the feature being processed.
Hello @UmaHarano, thanks for taking the time to dive into this.
I gave your suggestion a try but I can't see any difference with the new constructor: I still don't get any update on the dialog and, after I press the cancel button, the boolean progressorSource.Progressor.CancellationToken.IsCancellationRequested is always false (same as before).
Are you positive that you managed to get it work for the features other than the first one (in other words, that it updates the message on the dialog for every feature)?
In the meantime I added some code to my solution: there is a lot more going on inside the QueuedTask.Run() now, but I can confirm that despite all the stuff I do inside the cycle, if I just skip the call to the Export() method, the dialog works as expected (i.e. I get all the updates and the cancel request is handled correctly), so I really think there is something going on with using this method in conjunction with the CancelableProgressor.
Hi @Asimov
I had to edit your code to test (I removed the ClipAndRefreshMap, for example. I added my logic to export a custom extent instead) - If possible, can you share a complete addin with your code so I can try it out?
Here is a small video of what my sample does. (Attached)
Hi @UmaHarano,
in order to be able to run my solution you need quite some data, but I'll try to pack it all up, together with a few instructions that hopefully will speed you up.
For security reasons I'll send the url for the download in a direct message to you later during the day, since I can't post code and data in a public space.
Thanks again for your help, have a good day.
After sending the requested material, the discussion with @UmaHarano continued via private messages. In order to keep this topic updated, I'd like to briefly report what we found so far.
Uma was able to reproduce the issue, and she also spoke to a Pro developer about this. I don't know all the details, but it seems that currently this kind of scenario represent a limitation for the CancelableProgressorSource window. Uma was able to find a workaround, that basicly consists in adding a slight delay in the cycle after every message update:
progressorSource.Progressor.Value += 1;
progressorSource.Progressor.Message = $"Processing Tile {idTitle}...";
progressorSource.Progressor.Status = $"Processing Tile {idTitle}...";
Task.Delay((int)(500)).Wait();
This delay should give UI thread just a little extra time to update the progressor message. Unfortunately this solution is not really satisfactory for two main reasons: first of all in my case the cycle can involve thousands of items, resulting in a non-negligible amount of time lost. Furthermore, it seems to work only on the latest versions of the Pro (she's on 3.4), while it doesn't on my system (I'm on 3.2.2).
For the time being, I cannot update the Pro and I couldn't introduce such a delay anyway, so I managed to find another workaround in order to give the user the ability to cancel the operation. My solution is to wrap the Export function in a simple ProgressorSource (so I can print the right message for every tile), and expose a cancel button on a DockWindow in my UI: this button just sets a static boolean flag which is checked inside the cycle to be aware of the cancellation, in other words it does the job of the
progressorSource.Progressor.CancellationToken.IsCancellationRequested
This works: in between each export there is enough time to click the cancel button and, even if the CPS would be a much better and clean choice, it is a good compromise, given the situation.
I guess Uma will post any further update about this, I will do the same if I have any news.