MessageBox blocks UI and map window

1099
9
Jump to solution
08-23-2021 08:13 AM
RichardReinicke
Occasional Contributor II

Hi,

in my code I'm continously updating a map overlay. At some point of time I raise a MessageBox

ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show(
                            message,
                            "Kursverfolgung",
                            MessageBoxButton.OK,
                            MessageBoxImage.Information,
                            MessageBoxResult.OK
                        );


This makes my map window freezing and my overlay position don't gets updated anymore. Maybe it's a problem with WPF MessageBox itself but is there a way to show an into message to the user without blocking the UI?

There is no user interaction necessary. The info pane can disappear by itself let's say after 5 seconds.

Can I create a invisible pane, position it over the map view and make it visible for a short amount of time?

Thanks for any help.

0 Kudos
1 Solution

Accepted Solutions
ChristopherZent
Esri Regular Contributor
9 Replies
ChristopherZent
Esri Regular Contributor

Hi, the MapView Overlay Control would likely fit your needs. 

https://github.com/esri/arcgis-pro-sdk/wiki/ProConcepts-Map-Exploration#mapview-overlay-control 

RichardReinicke
Occasional Contributor II

Hello @ChristopherZent,

thank you for your link. I remembered there was something like that. 

But for some reason the MapViewOverlayControl is not visible for me. I also reinstalled the .Net SDK templates and utilities for VS2017 and tried with the embeddable control template but same effect. Maybe it's a thread problem again?

_handleGpsDataThread = new Thread(StartHandleGpsDataLoop);
_handleGpsDataThread.Priority = ThreadPriority.BelowNormal;
_handleGpsDataThread.Start();

private async void StartHandleGpsDataLoop()
{
    await HandleNewGpsDataAsync(GpsMessageBuffer.Last());
}

private async Task HandleNewGpsDataAsync(GpsData newGpsData)
{
    await QueuedTask.Run(() =>
    {
        try
        {
            ...
            var targetCourse = 
                GetDistanceToCurrentTarget(workstationLocation, 2).Result;
            ...
        }
        catch (EndOfRouteException eore)
        {
            var courseEndMessageControl = 
                new CourseEndMessageControlView();
            var mapViewOverlayControl =
                new MapViewOverlayControl(
                    courseEndMessageControl,
                    true, 
                    true, 
                    true,
                    OverlayControlRelativePosition.TopLeft);
                        
            MapView.Active.AddOverlayControl(mapViewOverlayControl);
        }
    }
}

private Task<Course> GetDistanceToCurrentTarget(MapPoint currentLocation, int decimalPrecision)
{
    return QueuedTask.Run(() =>
    {
        ...
        throw new EndOfRouteException(
            "You reached the end of the current route.");
        ...
    }
}

 

<UserControl x:Class="AddIn.Gps.UserControls.CourseEndMessageControlView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:ui="clr-namespace:AddIn.Gps.UserControls"
             xmlns:extensions="clr-namespace:ArcGIS.Desktop.Extensions;assembly=ArcGIS.Desktop.Extensions"
             mc:Ignorable="d" 
             d:DesignHeight="75" d:DesignWidth="415"
             d:DataContext="{Binding Path=ui.CourseEndMessageControlViewModel}">
            <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <extensions:DesignOnlyResourceDictionary Source="pack://application:,,,/ArcGIS.Desktop.Framework;component\Themes\Default.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <Grid>
        <Border Background="White" BorderBrush="LightSkyBlue" BorderThickness="2">
            <StackPanel Orientation="Vertical" >
                <TextBlock FontSize="20" FontWeight="Medium" Margin="20">
                    Sie haben den letzten Zielpunkt erreicht.
                </TextBlock>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>

 

Do you have any suggestion how I can make the overlay appear?

0 Kudos
Wolf
by Esri Regular Contributor
Esri Regular Contributor

To add the overlay control to the active map you can use this method: MapView.AddOverlayControl. This method takes a MapViewOverlayControl as its parameter.

   public void AddOverlayControl(IMapViewOverlayControl overlayControl)

Let me know if this works, otherwise I will create a sample snippet.

RichardReinicke
Occasional Contributor II

Hi @Wolf,

I added my relevant code under the first answer above. From my point of view that looks like what the documentation links contain.

We are at SDK version 2.4 and Pro version 2.4.1, due to customer decisions. Makes this a difference? Is my thread/task management the problem?

0 Kudos
Wolf
by Esri Regular Contributor
Esri Regular Contributor

I will try to get this to work with 2.4.1.

0 Kudos
ChristopherZent
Esri Regular Contributor

Here's an existing overlay control snippet that may help as well:

https://github.com/esri/arcgis-pro-sdk/wiki/ProSnippets-MapExploration#mapview-overlay-control 

0 Kudos
RichardReinicke
Occasional Contributor II

I was able to get this to work but I had to overcome a couple of problems. The main problem was indeed thread related.

The MapViewOverlayControl needs to be created on the main/UI thread. Furthermore the AddOverlayControl and RemoveOverlayControl methods need to be called on the same thread where the control has been created.

My solution is now a little heavy. I create the control when my map view is initialized.

 

_courseEndOverlay = 
    new MapViewOverlayControl(new CourseEndMessageControlView(), true, true, 
        true, OverlayControlRelativePosition.Center, 0.5, 0.5);

 

It sends a message from inner thread to outer with delegate callback but then I'm still not exactly on the same thread as my control ... so I need 

 

System.Windows.Application.Current.Dispatcher.Invoke((Action)delegate {
    _logger.Debug("show course end control");
    MapView.Active.AddOverlayControl(_courseEndOverlay);
});

await QueuedTask.Run(() => {
    Task.Delay(4000).Wait();
    System.Windows.Application.Current.Dispatcher.Invoke((Action)delegate {
        MapView.Active.RemoveOverlayControl(_courseEndOverlay);
    });
});

 

 

 

0 Kudos
Wolf
by Esri Regular Contributor
Esri Regular Contributor

You can use the following code to run actions on the UI thread - it is optimized as it first determines the 'thread context' the caller is running on:

 

 

/// <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 void RunOnUiThread(Action action)
{
    try
    {
        if (IsOnUiThread)
            action();
        else
            Application.Current.Dispatcher.BeginInvoke(action);
    }
    catch (Exception ex)
    {
        MessageBox.Show($@"Error in OpenAndActivateMap: {ex.Message}");
    }
}

/// <summary>
/// Determines whether the calling thread is the thread associated with this 
/// System.Windows.Threading.Dispatcher, the UI thread.
/// 
/// If called from a View model test it always returns true.
/// </summary>
public static bool IsOnUiThread => ArcGIS.Desktop.Framework.FrameworkApplication.TestMode || System.Windows.Application.Current.Dispatcher.CheckAccess();

 

 

You can then 'invoke' your UI actions like this:

 

 

// simple action
RunOnUiThread(() => { /* UI action functionality */ });

// action with await 
RunOnUiThread(async () => { /* UI action functionality with await */ });

 

 

I think in most cases you should refrain from using Dispatcher.Invoke because it's a blocking call (meaning it doesn't return to you until it has been executed), instead you should use Dispatcher.BeginInvoke which is non blocking.

Also there's another issue I noticed as the following code snippet with block the 'main CIM thread' for 4 seconds:

 

await QueuedTask.Run(() => {
    Task.Delay(4000).Wait();
    // delayed action
});

So I would recommend to run a 'delay' from an asynchronous task using something like this (this is a Pro button's OnClick method):

internal class TestDelay : Button
{
  protected override void OnClick()
  {
	_ = DelayTaskAsync();
	System.Diagnostics.Debug.WriteLine("OnClick done");
  }

  static async Task DelayTaskAsync()
  {
	Task delay = Task.Delay(5000);
	await delay;
	// do your delayed action here
	System.Diagnostics.Debug.WriteLine("Delayed action");
  }
}

This will not block the UI nor the CIM thread.

 

RichardReinicke
Occasional Contributor II

Very helpful @Wolf , thank you!

0 Kudos