Run multiple tasks async??

2031
9
Jump to solution
04-12-2021 10:15 AM
MKa
by
Occasional Contributor III

I am attempting to update a layout template by iterating through the text elements and changing the values of the template to something about the users map/settings.

I am able to loop through and change all the text elements, but I want to do all of the text elements at the same time.  I have a list of all the elements and the changes I want to make to each of them, but they always seem to run syncronously instead of in parellel?  What am i missing here?  The SetTextProperties is slow so I want to do about al 50 updates in parellel?

 //I have a list of object TextElementUpdate and updates to them
var tasks = new List<Task>();
                foreach (TextElementUpdate teu in textElementUpdates)
                {
//Shouldn't this line happen instantly and the whenall will wait?
//This line I want in parellel, so like 50 running at once, instead it is sync
                    tasks.Add(teu.UpdateTextElement());
                }

                //Wait for all the tasks to finish
                await Task.WhenAll(tasks);


//-------------------------------------------------------------------
class TextElementUpdate
        {
            public TextElementUpdate(TextElement textElement, TextProperties textProperties)
            {
                TextElementToUpdate = textElement;
                NewTextProperties = textProperties;
            }
            public TextElement TextElementToUpdate { get; set; }
            public TextProperties NewTextProperties { get; set; }

            public Task UpdateTextElement()
            {
                return QueuedTask.Run(() =>
                {
                    TextElementToUpdate.SetTextProperties(NewTextProperties);
                });
            }
        }

 

Tags (1)
0 Kudos
1 Solution

Accepted Solutions
CharlesMacleod
Esri Regular Contributor

you might be better off going against the layout CIM definition. See the code snippet below.

 

  var layout = LayoutView.Active?.Layout;
  if (layout == null)
    return;
  await QueuedTask.Run(() =>
  {
    var def = layout.GetDefinition() as CIMLayout;
    var graphic_elements = def.Elements.OfType<CIMGraphicElement>().ToList() 
          ??  new List<CIMGraphicElement>();
    bool changed = false;
    var when = DateTime.Now.ToString("G");
    int c = 0;
    foreach (var ge in graphic_elements)
    {
      if (ge.Graphic is CIMTextGraphic textGraphic) 
      {
           textGraphic.Text = $"Changed {c++}, {when}";
           changed = true;
      }
     }
     if (changed)
       //commit the changes
       layout.SetDefinition(def);
   });

 

If u want to find out more about the CIM take a look at this video:

ArcGIS Pro SDK for .NET: Understanding the CIM, a Guide for Developers 

View solution in original post

0 Kudos
9 Replies
MKa
by
Occasional Contributor III

Also there could be a way around this if maybe I could update all the textelements for a layout with one call?  Instead of looping through them and changing them.  Can't seem to be able to do that in bulk.

0 Kudos
MKa
by
Occasional Contributor III

I cant get this to work async?  I have a list of text elements from a layout (my print template), then loop through all of those textElements, get the name of the element, and change the text for that element to something from the users selected polygon attributes.

Is there a way to mass or bulk update the text elements?  i have to loop through and update about 50 text items for a layout template .pagx file.  I have tried updating the text properties and just using the CIMTextGraphic, and both seem very slow (around 40 seconds to do them all in total.

 

List<TextElementUpdate> textElementUpdates = new List<TextElementUpdate>();
foreach (Element ele in inLayoutElements)
{
	if (ele is TextElement)
	{
		TextElement textElement = ele as TextElement;
		string printTextValue = "";
		if (string.Equals(textElement.Name, "MyFieldName", StringComparison.OrdinalIgnoreCase))
		{
			printTextValue = "PolygonAttribute";
		}
		else if (string.Equals(textElement.Name, "MyFieldName2", StringComparison.OrdinalIgnoreCase))
		{
			printTextValue = "PolygonAttribute2";
		}
		//Repeat for all 50 fields or so
		//...
		//...

//After each element is found add the change to the object to process all at once after the loop
		CIMTextGraphic CIMTextGraphic = textElement.Graphic as CIMTextGraphic;
		CIMTextGraphic.Text = printTextValue;
		TextElementUpdate teu = new TextElementUpdate(textElement, CIMTextGraphic);
		textElementUpdates.Add(teu);
	}
}

//Update All the TextElements, this is slow to process about 50 text elements 
foreach (TextElementUpdate teu in textElementUpdates)
{
	teu.TextElementToUpdate.SetGraphic(teu.NewTextGraphic);
}
0 Kudos
CharlesMacleod
Esri Regular Contributor

you might be better off going against the layout CIM definition. See the code snippet below.

 

  var layout = LayoutView.Active?.Layout;
  if (layout == null)
    return;
  await QueuedTask.Run(() =>
  {
    var def = layout.GetDefinition() as CIMLayout;
    var graphic_elements = def.Elements.OfType<CIMGraphicElement>().ToList() 
          ??  new List<CIMGraphicElement>();
    bool changed = false;
    var when = DateTime.Now.ToString("G");
    int c = 0;
    foreach (var ge in graphic_elements)
    {
      if (ge.Graphic is CIMTextGraphic textGraphic) 
      {
           textGraphic.Text = $"Changed {c++}, {when}";
           changed = true;
      }
     }
     if (changed)
       //commit the changes
       layout.SetDefinition(def);
   });

 

If u want to find out more about the CIM take a look at this video:

ArcGIS Pro SDK for .NET: Understanding the CIM, a Guide for Developers 

0 Kudos
MKa
by
Occasional Contributor III

That worked brilliantly.  Exactly what I needed.  I couldn't figure out how to update the elements and set them all at once using set definition.  Now i need to find a way to export 10 layouts quicker.  I am looping through say 10 polygons, adding the feature attributes to my layout template (using the great code above) and exporting them.  This below export function will be my hang up now, as each call to export takes say (20-30 secs)

private static async Task ExportLayoutToFile(LayoutProjectItem inLayoutItem, string inExportFileName)
        {
            try
            {
                await QueuedTask.Run(() =>
                {
                    Layout testlayout = inLayoutItem.GetLayout();
                    if (testlayout == null)
                    {
                        return;
                    }

                    // Create PDF format with appropriate settings
                    PDFFormat PDF = new PDFFormat()
                    {
                        Resolution = 300,
                        OutputFileName = inExportFileName
                    };

                    if (PDF.ValidateOutputFilePath())
                    {
//THIS LINE IS MY HANG UP as I can't export multipls files at once?
                        testlayout.Export(PDF);
                    }
                });
            }
            catch (Exception e)
            {
                LogError("PrintForm - ExportLayoutToFile - ", e);
                return;
            }
        }
0 Kudos
CharlesMacleod
Esri Regular Contributor

export looks like a good candidate for the BackgroundTask so u might want to consider using _that_ rather than the QueuedTask. Obviously switching threads doesnt make something _quicker_ but it will free up the QueuedTask to allow u to get on with your other business.

 

  var layout = LayoutView.Active?.Layout;
  if (layout == null)
    return;
  await QueuedTask.Run(() =>
  {
    var def = layout.GetDefinition() as CIMLayout;
    //Make changes...
    //Commit changes
    layout.SetDefinition(def);

    //Tee-up export - add-in is responsible for checking
    //validity of OutputFileName
    var pdf = new PDFFormat()
    {
      Resolution = 300,
      OutputFileName = $"C:\\temp\\{layout.Name}.pdf"
    };
    //Export on a background thread
    ArcGIS.Core.Threading.Tasks.BackgroundTask.Run(
       ArcGIS.Core.Threading.Tasks.TaskPriority.normal,
       () => layout.Export(pdf),
       ArcGIS.Core.Threading.Tasks.BackgroundProgressor.None);
   //We are still on the QueuedTask here...
   //continue doing work but be mindful that your layout may
   //still be exporting
   ...
  });


    

 

U can read more about it here: https://github.com/esri/arcgis-pro-sdk/wiki/ProConcepts-Asynchronous-Programming-in-ArcGIS-Pro#using... . The most important thing to remember when using BackgroundTask is to never change application state_. That is what the CIM thread, or MCT as we call it is for, and that is accessed via QueuedTask.

 

0 Kudos
MKa
by
Occasional Contributor III

This will be great to run in the background.  However i cannot update past 2.5 yet.  We plan on doing that real soon, but that is not an option right now.  I might have to do a work around until we upgrade to 2.7 here in the next couple months.

0 Kudos
CharlesMacleod
Esri Regular Contributor

BackgroundTask is 2.6 fyi.

In the meanwhile u can try something like this - note: u cant use a Task directly because Tasks are MTA. Anyway, make sure u read all the fine print. Dont run anything off the MCT that alters the application. 

 

//http://stackoverflow.com/questions/16720496/set-apartmentstate-on-a-task
	internal class BackgroundTaskTemp
	{

		public static Task Run(TaskPriority priority, Action action)
		{
			var tcs = new TaskCompletionSource<bool>();
			Thread thread = new Thread(() =>
			{
				try
				{
					action();
					tcs.SetResult(true);
				}
				catch (Exception e)
				{
					tcs.SetException(e);
				}
			});

			thread.Priority = FromTaskPriority(priority);
			thread.SetApartmentState(ApartmentState.STA);
			thread.Start();
			return tcs.Task;

		}

		public static Task<T> Run<T>(TaskPriority priority, Func<T> func)
		{
			var tcs = new TaskCompletionSource<T>();
			Thread thread = new Thread(() =>
			{
				try
				{
					tcs.SetResult(func());
				}
				catch (Exception e)
				{
					tcs.SetException(e);
				}
			});

			thread.Priority = FromTaskPriority(priority);
			thread.SetApartmentState(ApartmentState.STA);
			thread.Start();
			return tcs.Task;
		}

		private static ThreadPriority FromTaskPriority(TaskPriority priority)
		{
			if (priority == TaskPriority.high)
				return ThreadPriority.Highest;
			if (priority == TaskPriority.single)
				throw new InvalidOperationException("Sorry, no can do - wait for 2.6 and the proper BackgroundTask");
			else return ThreadPriority.Normal;
		}
	}
}

 

usage:

 

 var layout = LayoutView.Active?.Layout;
 if (layout == null)
   return;
 await QueuedTask.Run(() =>
 {
   ...
   //export on the background thread				 
   //ArcGIS.Core.Threading.Tasks.BackgroundTask.Run(
   //ArcGIS.Core.Threading.Tasks.TaskPriority.normal,
   //	() => layout.Export(pdf),
   //ArcGIS.Core.Threading.Tasks.BackgroundProgressor.None);

   BackgroundTaskTemp.Run(				 
     ArcGIS.Core.Threading.Tasks.TaskPriority.normal,
    () => layout.Export(pdf));
    ...
    ...
});

 

0 Kudos
MKa
by
Occasional Contributor III

This looks very promising to what i want to do.  The thing is, is that I might be updating say 1-10 layouts at a time, so what I want to do is export them all at once on their own background thread, then wait for them all to finish.  So i think the only way to do that would be to save my "Updated" layout as a clone or something.  Otherwise I will just be printing the last layout 10 times? 

So right now, in a loop i update the layout using polygon attributes, then export it, then move to the next.  The export being the choke point.  So for 10 exports it's 10x time.

What i think your above process will let me do, is build up 10 layout clones in that loop, then export them all at once, drastically reducing the export time to 1x or 2x time.  But I think I need to clone the layouts that I update the elements on, then export them, then dispose them.  

0 Kudos
MKa
by
Occasional Contributor III

I tried to run 2 exports using your above Background method and I got the GPU error in ArcGIS Pro.  I don't think it was a coincidence.  I tried this with the same layout to see if I could process multiple exports simultaniously.  

Task btt = BackgroundTaskTemp.Run(TaskPriority.normal, () => testlayout.Export(PDF));
Task btt2 = BackgroundTaskTemp.Run(TaskPriority.normal, () => testlayout.Export(PDF2));
await btt;
await btt2;

This above is essentially what I am trying to do.  A user selects multiple polygons, and I will export those as Maps to print.  They can select 1 to 10 maps.

 

 

0 Kudos