Route does not draw on first attempt, but does subsequently

1077
20
05-30-2019 10:22 AM
ITS_JohnColaneri
New Contributor

env:

Xamarin Android on Visual Studio 17 - min API level 21, target - 27, TLS 1.2 settings/ testing on Samsung Galaxy S8 device

I've been struggling for some time with an issue where: when loading a map and attempting to draw a route, the route draw fails (just doesn't draw it - pretty sure the route-solve is not throwing any errors). If I back-up in the GUI, and initiate the map/route draw again, the subsequent attempts succeed.

I "back-burner"-ed this bug for a couple months to work on other aspects of the app. I observed that the map was "Loaded," but not drawn before the route-solve started, so I thought "Ah ha!" I've switched the code around to wait for the "DrawStatus.Completed" event instead, (and made it quasi-idempotent with a bool switch so the route only gets initiated once). I'm still getting the same result though. First time through, no route. All subsequent attempts are fine (if a little sluggish).

Can anyone help with this?

0 Kudos
20 Replies
dotMorten_esri
Esri Frequent Contributor

Do you have a sample you could share that demonstrates this behavior?

0 Kudos
ITS_JohnColaneri
New Contributor

How's this?

Well,... there are a lot of files, but here are the relevant excerpts:

* Invoker *:

<<Activity>>

protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);

// Create your application here
if (Intent.HasExtra("MapPoints"))
   _mapPointString = Intent.Extras.GetString("MapPoints");
if (Intent.HasExtra("RelocatePoint"))
   _relocatePoint = Intent.GetBooleanExtra("RelocatePoint", false);

if (Intent.HasExtra("Routing") && Intent.GetBooleanExtra("Routing", true))
{
   SetContentView(Resource.Layout._esrimap_layout);
   _mapFrag = FragmentManager.FindFragmentById<CMIPEsriMapFragment>(Resource.Id.esrimap);
   Task.Run(() => {

      _mapFrag.DrawRoute = true;
      _mapFrag.Setup(_mapPointString, _relocatePoint);

   }).ContinueWith(t => {

      << Exception Stuff >> ...

   }, TaskContinuationOptions.OnlyOnFaulted);

}
else
{
   try
   {...

<<Fragment>>

public void Setup(string mapPointsJsonString, bool relocatePoint)
{...

   Task.Run(async () =>
   {
      if (_map == null) // get a freshy
      {
         await _map.LoadAsync();
      }...

      << Exception Stuff >> ...

private void OnMapVisible(object sender, DrawStatusChangedEventArgs e)
{
   if (e.Status == DrawStatus.Completed)
   {
      if (DrawRoute)
      {
         DoRoute();
         DrawRoute = false; // Quasi idempotent :\
      }
   }
}

* The Meat *:

private void InitAuth()
{
   // Get a reference to the (singleton) AuthenticationManager for the app
   AuthenticationManager authMgr = AuthenticationManager.Current;

   // Assign the method that AuthenticationManager will call to challenge for secured resources
   authMgr.ChallengeHandler = new ChallengeHandler(CreateCredentialAsync);
}

private async Task<Credential> CreateCredentialAsync(CredentialRequestInfo info)
{
   try
   {
      // See if authentication is already in process
      if (_taskCompletionSource != null)
      {
         return null;
      }

      // Create a new TaskCompletionSource for the login operation.
      // Passing the CredentialRequestInfo object to the constructor will make it available from its AsyncState property.
      _taskCompletionSource = new TaskCompletionSource<Credential>(info);

      await LoginOtherStuff();
      // Return the login task, the result will be ready when completed (LoginOtherStuff method gets called).
      return await _taskCompletionSource.Task;
   }
   catch(AggregateException ae)
   {
      << Exception Stuff >> ...
   }
}

public async Task LoginOtherStuff()
{
try

   // Get the associated CredentialRequestInfo (will need the URI of the service being accessed).
   CredentialRequestInfo requestInfo = _taskCompletionSource.Task.AsyncState as CredentialRequestInfo;

   // Create a token credential using the provided username and password.
   TokenCredential userCredentials = await AuthenticationManager.Current.GenerateCredentialAsync
      (requestInfo.ServiceUri,
      Resources.GetText(Resource.String.hotdog),
      Resources.GetText(Resource.String.fish_taco),   // I know, I know... You can't install the app.
      requestInfo.GenerateTokenOptions);

   // Set the result on the task completion source.
   _taskCompletionSource.TrySetResult(userCredentials);
}
catch (AggregateException ae)
{

   << Exception Stuff >> ...

}
}

0 Kudos
dotMorten_esri
Esri Frequent Contributor

This bit looks suspicions wrt on first load:

   Task.Run(async () =>
   {
      if (_map == null) // get a freshy
      {
         await _map.LoadAsync();
      }...

I'm not seeing that you are awaiting the map load, so perhaps the code execution is significantly different when map is null the first time?

0 Kudos
ITS_JohnColaneri
New Contributor

Sorry, here's the rest of it:

   

Task.Run(async () =>
   {
      if (_map == null) // get a freshy
      {
         await _map.LoadAsync();
      }...

   }).ContinueWith(t =>
   {

      << Exception Stuff >> ...

    }, TaskContinuationOptions.OnlyOnFaulted);

0 Kudos
dotMorten_esri
Esri Frequent Contributor

But where's the code that actually draws stuff? When is it called?

And is there a reason you don't just use async/await instead of Task.Run? (without it, it can get easy to swallow errors)

public async Task Setup(string mapPointsJsonString, bool relocatePoint)
{

  try

  {

      if (_map == null)
      {
         await _map.LoadAsync();
      }

  }

  catch(

  {

      << Exception Stuff >> ...

   }

}

In your oncreate method I also see you're using Task.Run, but I don't really see why you're doing that.

0 Kudos
ITS_JohnColaneri
New Contributor

Sorry, my bad. There's a lot of gobble-de-gook in here. Map is created in Fragment.OnCreate():

public override void OnCreate(Bundle savedInstanceState)
{
   base.OnCreate(savedInstanceState);
   // Create your fragment here
   DrawRoute = false;
   _myMapView = new MapView(Activity);
   _myMapView.InteractionOptions = new MapViewInteractionOptions
   {
      IsMagnifierEnabled = false//,
   };
   Basemap bm = new Basemap(new       Uri("https://services.arcgisonline.com/ArcGIS/rest/services/USA_Topo_Maps/MapServer"));
   _map = new Map(bm);

   _myMapView.Map = _map;

}

-- Drawing in DoRoute():

public async void DoRoute()
{
   InitAuth();
   _myMapView.LocationDisplay.IsEnabled = true; // to facilitate taking of the device location
   var pos = _myMapView.LocationDisplay.Location.Position; // device current location.
   _myMapView.LocationDisplay.ShowLocation = false;
   MapPoint circleCenter = new MapPoint(pos.X, pos.Y, SpatialReferences.Wgs84);
   CFMapPoint youLocation = PointUtilities.ToCFMapPoint(circleCenter);
   youLocation.NavigateTo = true;
   youLocation.Icon = Resource.Drawable.you;

   Stop start = new Stop(circleCenter);
   Stop stop = new Stop(PointUtilities.ToMapPoint(_mapPoints[0]));
   _routeStops = new List<Stop> { start, stop };

   _routeGraphicsOverlay = new GraphicsOverlay() { Id = Resources.GetText(Resource.String.route_overlay) };

   try
   {
      RouteTask solveRouteTask = await RouteTask.CreateAsync(
         new Uri("https://route.arcgis.com/arcgis/rest/services/World/Route/NAServer/Route_World"));
      RouteParameters routeParams = await solveRouteTask.CreateDefaultParametersAsync();
      routeParams.SetStops(_routeStops);
      RouteResult solveRouteResult = await solveRouteTask.SolveRouteAsync(routeParams);
      Route firstRoute = solveRouteResult.Routes.FirstOrDefault();

      Activity.RunOnUiThread(() =>
      {

         Polyline routePolyline = firstRoute.RouteGeometry;
         SimpleLineSymbol routeSymbol =

            new SimpleLineSymbol(SimpleLineSymbolStyle.Solid, Color.SlateBlue, 4.0);
         Graphic routeGraphic = new Graphic(routePolyline, routeSymbol);

         _routeGraphicsOverlay.Graphics.Add(routeGraphic);
         _myMapView.GraphicsOverlays.Insert(0, _routeGraphicsOverlay);
         // adjust the envelope in such case the route goes off the screen
         if (_markerOverlay.Extent.Width < _routeGraphicsOverlay.Extent.Width * 1.15 ||
            _markerOverlay.Extent.Height < _routeGraphicsOverlay.Extent.Height * 1.15)
         {
            EnvelopeBuilder myEnvelopeBuilder = new EnvelopeBuilder(_routeGraphicsOverlay.Extent);
            myEnvelopeBuilder.UnionOf(_markerOverlay.Extent);
            myEnvelopeBuilder.Expand(1.15);
            _myMapView.SetViewpointAsync(new Viewpoint(myEnvelopeBuilder.Extent));
         }

         _taskCompletionSource = null;
      });
   }
   catch (AggregateException ae)
   {
      foreach (var ex in ae.InnerExceptions)
         ...

I'll have to ruminate the other question. Kind of a noob. The original architectural framework came from other noob went "bye-bye." I just refactored for Esri maps, and snuffled for memory leaks and bugs and such.

0 Kudos
dotMorten_esri
Esri Frequent Contributor

WIthout a more full example that can be debugged I'll just be guessing here. But the fact that it works except for the first time, indicates some of your async initialization code is problematic, and where you should look first.

0 Kudos
ITS_JohnColaneri
New Contributor

Well, my [limited] understanding of async methods is that they have to be called/awaited from another async method. With such a scheme, you just go on async-ing the callers forever. I'm not going to make the life cycle methods async. My attitude is that you have to cauterize it somewhere. The technique that I've read about on c# forums is to start a Task and let the async junk run in it's own thread off to the side.

This project is 55 source code files not including resources. I'm not at leisure to go dumping it on the internet.

Thanks for the advice.

0 Kudos
dotMorten_esri
Esri Frequent Contributor

> my [limited] understanding of async methods is that they have to be called/awaited from another async method. 

No that's not the case. You're just adding more layers of async'ness, more thread jumping etc. That also means you're less likely to know what thread you're own and getting yourself into more problems. You definitely shouldn't be launching a task just to run a task.

>  I'm not going to make the life cycle methods async.

There's no problem with this, and you should. It's true you should avoid "async void" and use "async task" when you can, but of course overrides it's not possible, and you normally handle that with try/catch to ensure you don't throw out of context.

This project is 55 source code files not including resources. I'm not at leisure to go dumping it on the internet.

I wasn't suggesting that. A small sample that reproduces the problem instead.

I can recommend this short 6-part series (they are short) on async/await: 

https://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/

0 Kudos