Map Loaded but DrawStatus InProgress and UI freeze

2446
9
07-19-2019 06:48 AM
DavidBeni
New Contributor III

Hi,
I have a webmap which I use in a mobile app with Xamarin Forms, ArcGIS and Prism .NET Runtime 100.5.

Often my MapView is loaded (Map.LoadStatus → Loaded) but the draw status remains in progress (DrawStatus → InProgress), (the application show a blank grid with the attribution) and freeze the UI. Sometimes all works fine (Map.LoadStatus → Loaded & DrawStatus → Completed). The issue is independent of the web connection and come from the mapView.
There are no other threads I invoke in my code that could freeze the UI.

The problem persists despite different implementations.
I followed the following sample: Display a map—ArcGIS Runtime SDK for .NET | ArcGIS for Developers 

XAML :

<viewmodels:MapViewViewModel x:Name="mapView" />

MapViewViewModel.cs :

public class MapViewViewModel : MapView, INotifyPropertyChanged
{
        public MapViewViewModel()
        {

            const string WebMapId = "..." ;
            DrawStatusChanged += OnDrawStatusChanged;

            Device.BeginInvokeOnMainThread(async () =>
            {
                var portal = await ArcGISPortal.CreateAsync(new Uri("http://www.arcgis.com/sharing/rest"));
                var portalItem = await PortalItem.CreateAsync(portal, WebMapId);
                Map = new Esri.ArcGISRuntime.Mapping.Map(portalItem);
                await Map.LoadAsync();
            });
        }

        private async void OnDrawStatusChanged(object sender, DrawStatusChangedEventArgs e)
        {
            Console.WriteLine(e.Status.ToString());
        }
}

Thanks,

0 Kudos
9 Replies
marceloctorres
Esri Contributor

Why MapViewModel is a inherited class of MapView, I think it is not a true MVVM implementation... 

Marcelo César Torres
0 Kudos
marceloctorres
Esri Contributor
0 Kudos
marceloctorres
Esri Contributor
using Esri.ArcGISRuntime.UI;
using Esri.ArcGISRuntime.Xamarin.Forms;
using System.Windows.Input;
using Xamarin.Forms;
namespace VisorMapaFSM.Behaviors
{
  /// <summary>
  /// Clase que implementa el manejo del evento <b>MapView.DrawStatusChanged</b> de una instancia <see cref="MapView"/> y ejecuta un
  /// <see cref="Comando"/> si está definido, y está enlazado con una propiedad de una clase cuyo tipo implementa la interfaz <see cref="ICommand"/>.
  /// </summary>
  /// <example>
  /// En el código XAML de un ContentPage:
  /// <code language="XAML" title="XAML">
  /// <![CDATA[
  ///    <esriUI:MapView x:Name="MapView"
  ///                 Map="{Binding Map}"
  ///                  LocationDisplay="{Binding LocationDisplay, Mode=TwoWay}"
  ///                  GraphicsOverlays="{Binding CapasGraficos}">
  ///        <esriUI:MapView.GraphicsOverlays>
  ///       </esriUI:MapView.GraphicsOverlays>
  ///        <esriUI:MapView.Behaviors>
  ///          <bh:SetMapViewViewportBehavior Viewpoint = "{Binding NuevoPuntoVista}" />
  ///          <bh:IdentifyGraphicsBehavior Comando = "{Binding IdentificarOrdenTrabajoCommand}" />
  ///          <bh:DrawingStatusChangedBehavior Comando = "{Binding CambiarVisibilidadProgresoCommand}" />
  ///          <bh:MapViewViewpointChangedBehavior Comando = "{Binding ActualizarPuntoVistaCommand}" />
  ///        </esriUI:MapView.Behaviors>
  ///      </esriUI:MapView>
  /// ]]>
  /// </code>
  /// En la clase que implementa el ViewModel del ContentPage:
  /// <code language="C#" title="C#">
  /// <![CDATA[
  ///   public ICommand IdentificarOrdenTrabajoCommand { get; private set; }
  ///   public ICommand CambiarVisibilidadProgresoCommand { get; private set; }
  ///   public ICommand ActualizarPuntoVistaCommand { get; private set; }
  ///   private void InicializarComandos()
  ///   {
  ///     this.IdentificarOrdenTrabajoCommand = new DelegateCommand<GraficoConsultaInfo>(this.IdentificarOrdenTrabajoAction);
  ///     this.CambiarVisibilidadProgresoCommand = new DelegateCommand<bool?>(this.CambiarVisibilidadProgresoAction);
  ///     this.ActualizarPuntoVistaCommand = new DelegateCommand<Viewpoint>(this.ActualizarPuntoVistaAction);
  ///   }
  /// ]]>
  /// </code>
  /// </example>
  public class DrawingStatusChangedBehavior : BehaviorBase<MapView>
  {
    /// <summary>
    /// Implementa una propiedad enlazable que provee la interfaz para la propiedad <see cref="Comando"/>.
    /// </summary>
    public static readonly BindableProperty ComandoProperty = BindableProperty.Create(
        nameof(Comando),
        typeof(ICommand),
        typeof(DrawingStatusChangedBehavior)
      );
    /// <summary>
    /// Instancia de un objeto que implementa la interfaz <see cref="ICommand"/> que se ejecutará como consecuencia del manejo
    /// del evento <b>MapView.DrawStatusChanged</b> del objeto <see cref="MapView"/> asociado con una instancia del
    /// comportamiento.
    /// </summary>
    public ICommand Comando
    {
      get { return (ICommand)GetValue(ComandoProperty); }
      set { SetValue(ComandoProperty, value); }
    }
    /// <summary>
    /// Implementa el comportamiento que se adjunta al objeto <paramref name="bindable"/> de tipo <see cref="MapView"/>.
    /// </summary>
    /// <param name="bindable"></param>
    protected override void OnAttachedTo(MapView bindable)
    {
      base.OnAttachedTo(bindable);
      bindable.DrawStatusChanged += DrawStatusChanged;
    }
    /// <summary>
    /// Implementa la remoción del comportamiento del objeto <paramref name="bindable"/> de tipo <see cref="MapView"/>.
    /// </summary>
    /// <param name="bindable"></param>
    protected override void OnDetachingFrom(MapView bindable)
    {
      base.OnDetachingFrom(bindable);
      bindable.DrawStatusChanged -= DrawStatusChanged;
    }
    /// <summary>
    /// Si el Comando está definido lo ejecuta, como respuesta al evento <b>MapView.DrawStatusChanged</b>.
    /// El Comando se ejecuta con un argumento de tipo <see cref="System.Boolean"/> que indica si el objeto del tipo <see cref="MapView"/>
    /// se está dibujando.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void DrawStatusChanged(object sender, Esri.ArcGISRuntime.UI.DrawStatusChangedEventArgs e)
    {
      if(this.Comando != null)
      {
        bool enProgreso = e.Status == DrawStatus.InProgress;
        if(this.Comando.CanExecute(enProgreso))
        {
          this.Comando.Execute(enProgreso);
        }
      }
    }
  }
}
Marcelo César Torres
0 Kudos
DavidBeni
New Contributor III

Thanks but,
Another implementation, same problem
:

XAML :

<esriUi:MapView Map="{Binding Map, Source={StaticResource MapViewModel}}" x:Name="mapView" />

MapViewModel.cs :

public class MapViewModel : BindabledBase
{

      private const string WebMapId = "..." ;
      private Map _map;
      public Map Map
      {

         get { return _map; }
         set { SetProperty(ref _map, value); }
      }

      public MapViewModel()
      {

         Task.Run(async () =>
          {
                var portal = await ArcGISPortal.CreateAsync(new Uri("http://www.arcgis.com/sharing/rest"));
                var portalItem = await PortalItem.CreateAsync(portal, WebMapId);
                var map = new Esri.ArcGISRuntime.Mapping.Map(portalItem);
                await map.LoadAsync();
                if (map.LoadStatus != LoadStatus.FailedToLoad) //Always True for me, but sometimes mapView Drawstatus is never Completed
                {
                    Device.BeginInvokeOnMainThread(() => {              
                        Map = map;
                    });
                }
         });
      }
}

0 Kudos
DavidBeni
New Contributor III

Doesn't work properly either in the following implementation:

MainPage.xaml :

<esriUI:MapView x:Name="mapView" />

MainPage.xaml.cs :

public partial class MainPage : ContentPage
{

      public MainPage()

      {

         NavigationPage.SetHasNavigationBar(this, false);
         InitializeComponent();
         InitializeMap();

      }

      private void InitializeMap()

      {

         const string WebMapId = "9801e...6553" ;
         Task.Run(async () =>
         {

            var portal = await ArcGISPortal.CreateAsync(new Uri("http://www.arcgis.com/sharing/rest"));
            var portalItem = await PortalItem.CreateAsync(portal, WebMapId);
            var _map = new Esri.ArcGISRuntime.Mapping.Map(portalItem);
            if (_map.LoadStatus != LoadStatus.FailedToLoad)

            {

               Device.BeginInvokeOnMainThread(() => {

                  mapView.Map = _map;

               });

            }

         }); 

      }
}

0 Kudos
marceloctorres
Esri Contributor

You don't have to set the Map property inside of BeginInvokeOnMainThread method. At the first implementation you just set Map property directly: Map = new Esri.ArcGISRuntime.Mapping.Map(portalItem), instead of using the map auxiliary variable. The DataBinding mechanism takes care of assign the Map value to the MapView control and this takes care of start the process of loading the map and all its layers.

Marcelo César Torres
0 Kudos
marceloctorres
Esri Contributor

I assume that you're working with Prism Library from Brian Lagunas NuGet Gallery | Prism.Forms 7.2.0.1367 . In this library there is a behavior to convert Events fired by controls in views, to a Commands Properties in the behavior that can be databinded with Commands Properties in view-models.

See this example:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:esriUI="clr-namespace:Esri.ArcGISRuntime.Xamarin.Forms;assembly=Esri.ArcGISRuntime.Xamarin.Forms"
             xmlns:prism="http://prismlibrary.com"
             x:Class="GeoNet.DrawStatus.Views.MainPage"
             Title="{Binding Title}">
  <Grid>
    <esriUI:MapView x:Name="mapView" 
                    Map="{Binding Map}">
      <esriUI:MapView.Behaviors>
        <prism:EventToCommandBehavior EventName="DrawStatusChanged"
                                      EventArgsParameterPath="Status"
                                      Command="{Binding DrawStatusChangedCommand}" />
      </esriUI:MapView.Behaviors>
    </esriUI:MapView>
    <ActivityIndicator IsVisible="{Binding IsActivityVisible}"
                       IsRunning="{Binding IsActivityVisible}" />
  </Grid>

</ContentPage>

EventToCommandBehavior was added to MapView control in the view. Each time MapView fires a "DrawStatusChanged" event, the behavior intercepts it, takes the Status property of DrawStatusChangedEventArgs parameter of the event and passes to ViewModel through CommandParameter property of the DrawStatusChangedCommand. There was a ActivityIndicatorControl addeded to the ContentPage, it's IsVisible and IsRunning properties have DataBindings to the IsActivityVisible property of the ViewModel. 

The Constructor of the ViewModel class has the definition of an Action to handle calls to DrawStatusChangedCommand made from the EventToCommandBehavior added to the MapView control. Inside the Action, IsActivityVisible property of the viewmodel is changed depending on the value of the e parameter of type DrawStatus sent to it. This makes that the ActivityIndicator be visible when the DrawStatus is equal to InProgess, and invisible when is iqual to Completed. In order to made async load of the map inside the SetMap() method, the async clause must be added to it, although, as mentioned, this call is not necessary.

using System;
using System.Diagnostics;
using System.Windows.Input;

using Esri.ArcGISRuntime.Mapping;

using Prism.Commands;
using Prism.Navigation;

using ArcGisRuntimeUI = Esri.ArcGISRuntime.UI;

namespace GeoNet.DrawStatus.ViewModels
{
  /// <summary>
  /// 
  /// </summary>
  public class MainPageViewModel : ViewModelBase
  {

    private Map _map;
    private bool _isActivityVisible;

    /// <summary>
    /// 
    /// </summary>
    public Map Map
    {
      get { return _map; }
      set { SetProperty(ref _map, value); }
    }

    /// <summary>
    /// 
    /// </summary>
    public bool IsActivityVisible
    {
      get { return _isActivityVisible; }
      set { SetProperty(ref _isActivityVisible, value); }
    }

    /// <summary>
    /// 
    /// </summary>
    public ICommand DrawStatusChangedCommand { get; private set; }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="navigationService"></param>
    public MainPageViewModel(INavigationService navigationService)
        : base(navigationService)
    {
      Title = "Main Page";

      DrawStatusChangedCommand = new DelegateCommand<ArcGisRuntimeUI.DrawStatus?>((e) =>
      {
        if (e.HasValue)
        {
          IsActivityVisible = e.Value != ArcGisRuntimeUI.DrawStatus.Completed;
        }
      });
      SetMap();
    }

    private void SetMap()
    {
      try
      {
        Map = new Map(BasemapType.StreetsNightVector, 4.5, -74.5, 10);
        Map.Loaded += (o, e) =>
        {
          Debug.WriteLine("Map");
        };
        //await Map.LoadAsync();

      }
      catch(Exception ex)
      {
        Debug.WriteLine(ex.Message);
      }
    }
  }
}

Marcelo César Torres
0 Kudos
DavidBeni
New Contributor III

Hi,
I know that.
But that's not the problem. The problem remains open

0 Kudos
JoeHershman
MVP Regular Contributor

Seems like a lot of complicated code.  I have never seen the behavior you describe, and have never had to track Draw Status.  Simply have a ViewModel attached to the view with MapView with a bound Map and open the package and set the Map property.  Don't even have to do anything with the binding because I just use the Prism naming convention.  You definitely 100% do not want to be inheriting your view model from MapView.

I have changed things around a bit because I created a shared MapView control in order to Incorporate custom behavior and use in different views in multiple modules 

<?xml version="1.0" encoding="utf-8" ?>
<esri:MapView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             xmlns:esri="clr-namespace:Esri.ArcGISRuntime.Xamarin.Forms;assembly=Esri.ArcGISRuntime.Xamarin.Forms"
             xmlns:mapView="clr-namespace:Mobile.AsBuilt.Framework.MapView;assembly=Mobile.AsBuilt.Framework"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="Mobile.AsBuilt.Framework.MapView.MapViewControl"
             x:Name="MapView" 

             Map="{Binding Map}"
                   SketchEditor="{Binding SketchEditor}"
                   GraphicsOverlays="{Binding GraphicsOverlays}"


             GeoViewTapped="MapViewControl_OnGeoViewTapped"
             GeoViewDoubleTapped="MapViewControl_OnGeoViewDoubleTapped"
             GeoViewHolding="MapViewControl_OnGeoViewHolding"
             ViewpointChanged="MapViewControl_OnViewpointChanged"
             NavigationCompleted="MapViewControl_OnNavigationCompleted">


</esri:MapView>‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

Because of the control approach what I do is open the map package in a different view model and then just kick it to the MapViewControl view model via an event.  Previously when the MapView was in the main view model I just set the Map property in the OnItialized event

public class MapViewControlViewModel : BindableBase
{
	#region Private Fields

	private IEventAggregator _eventAggregator;
	private ILocationProvider _locationProvider;
	private ILogger _log;
	private Map _map;
	private SketchEditor _sketchEditor;

	#endregion


	public MapViewControlViewModel(IEventAggregator eventAggregator, ILogManager logManager, ILocationProvider locationProvider)
	{
		//TODO:  Setup with subscription token
		eventAggregator.GetEvent<MapLoadedEvent>().Subscribe(OnMapLoaded);
		eventAggregator.GetEvent<NavigatedFromEvent>().Subscribe(OnNavigatedFrom);
		eventAggregator.GetEvent<AddGraphicsOverlayEvent>().Subscribe(OnAddGraphicsOverlay);

		EventAggregator = eventAggregator;
	}

	private void OnAddGraphicsOverlay(GraphicsOverlay go)
	{
		if (!GraphicsOverlays.Contains(go))
		{
			GraphicsOverlays.Add(go);
		}
	}
	
	public IEventAggregator EventAggregator
	{
		get => _eventAggregator;
		set => SetProperty(ref _eventAggregator, value);
	}

	public ILogger Log
	{
		get => _log;
		set => SetProperty(ref _log, value);
	}

	public ILocationProvider LocationProvider
	{
		get => _locationProvider;
		set => SetProperty(ref _locationProvider, value);
	}


	public GraphicsOverlayCollection GraphicsOverlays { get; set; } = new GraphicsOverlayCollection();

	public SketchEditor SketchEditor
	{
		get => _sketchEditor;
		set => SetProperty(ref _sketchEditor, value);
	}

	public Map Map
	{
		get => _map;
		set => SetProperty(ref _map, value);
	}

	private void OnMapLoaded(MapLoadedEventArgs obj)
	{
		try
		{
			Map = obj.Map;
			EventAggregator.GetEvent<SketchEditorInitializedEvent>().Publish(SketchEditor);
		}
		catch (Exception e)
		{
			Log.Error(e.Message, e);
		}
	}

	private void OnNavigatedFrom()
	{
		Map = null;
		SketchEditor = null;
		GraphicsOverlays.Clear();
		EventAggregator.GetEvent<MapLoadedEvent>().Unsubscribe(OnMapLoaded);
		EventAggregator.GetEvent<AddGraphicsOverlayEvent>().Unsubscribe(OnAddGraphicsOverlay);
	}
}
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
Thanks,
-Joe
0 Kudos