Select to view content in your preferred language

esritoolkit:BasemapGallery - AvailableBasemaps customization does not work

1534
10
Jump to solution
02-02-2023 06:18 AM
ViktorSafar
Frequent Contributor

I have got a WPF MVVM app where I have a MapView, and the BasemapGallery from the toolkit:

 

        <esri:MapView 
            x:Name="MainMapView" 
            Map="{Binding Map}"
            />

        <esritoolkit:BasemapGallery
            x:Name="BasemapGallery"   
            GeoModel="{Binding ElementName=MainMapView, Path=Map}"
            AvailableBasemaps="{Binding AvailableBasemaps}"/>

 

The Map property on the VM:

 

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


// initialized in VM constructor
Map = new Map
{
	InitialViewpoint = new Viewpoint(new MapPoint(X, Y, spatialReference: SpatialReference.Create(3857)), 130617),
	Basemap = new Basemap(BasemapStyle.ArcGISTopographic)
};

 

Changing the mapview's base map from the gallery works fine. The gallery loads base maps from AGOL which is something I would like to override. The app will be on an offline device so I have some map packages locally but I am currently testing with a VectorTileServer base map:

 

var vectorTiledLayer = new ArcGISVectorTiledLayer(new Uri("https://server/arcgis/rest/services/MyService/VectorTileServer"));
var newBaseMap = new Basemap(vectorTiledLayer)
var item = await BasemapGalleryItem.CreateAsync(newBaseMap);
AvailableBasemaps = new List<BasemapGalleryItem>();
AvailableBasemaps.Add(item);


// where AvailableBasemaps is:
private IList<BasemapGalleryItem> _availableBasemaps;
public IList<BasemapGalleryItem> AvailableBasemaps
{
	get { return _availableBasemaps; }
	set { SetProperty(ref _availableBasemaps, value); }
}

 

The gallery is still showing the AGOL base maps, and not at all my custom base map.

What am I doing wrong?

 

Esri.ArcGISRuntime.WPF v100.15.0

Esri.ArcGISRuntime.Toolkit v100.15.0

0 Kudos
1 Solution

Accepted Solutions
ViktorSafar
Frequent Contributor

If anyone's using Prism, there is a dead simple way to make your own gallery

 

XAML:

 

 

<UserControl x:Class="MapGallery.MapGalleryUC"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"             
             prism:ViewModelLocator.AutoWireViewModel="True">
    <ListView
        ItemsSource="{Binding AvailableMaps}"
        SelectedValue="{Binding SelectedMap}"        
        >
        <ListView.ItemTemplate>
            <DataTemplate>
                <WrapPanel>
                    <Image Source="{Binding Thumbnail}" Height="100" Width="100" />
                    <Label Content="{Binding Basemap.Name}" VerticalContentAlignment="Center"/>
                </WrapPanel>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</UserControl>

 

 

 

VM:

 

 

internal class MapGalleryVM : BindableBase
{
	private readonly IEventAggregator _eventAggregator;

	public MapGalleryVM(IEventAggregator eventAggregator)
	{
		_eventAggregator = eventAggregator;
		_eventAggregator.GetEvent<BasemapsLoadedEvent>().Subscribe(HandleBasemapsLoadedEvent);
	}

	private void HandleBasemapsLoadedEvent(List<AppBasemap> appMaps)
	{
		var results = appMaps.Select(x => new MapGalleryModel()
		{
			Basemap = x.Basemap,
			Thumbnail = new BitmapImage(new System.Uri(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, x.MapFileConfig.Thumbnail)))
		});

		_ = Application.Current.Dispatcher.BeginInvoke(() =>
		{
			AvailableMaps = new ObservableCollection<MapGalleryModel>(results);
		});
	}

	private ObservableCollection<MapGalleryModel> _availableMaps;
	public ObservableCollection<MapGalleryModel> AvailableMaps
	{
		get { return _availableMaps; }
		set { SetProperty(ref _availableMaps, value); }
	}


	private MapGalleryModel _selectedMap;
	public MapGalleryModel SelectedMap
	{
		get { return _selectedMap; }
		set 
		{ 
			if (value != SelectedMap)
			{
				SetProperty(ref _selectedMap, value);
				_eventAggregator.GetEvent<BasemapChangedEvent>().Publish(value.Basemap);
			}
		}
	}
}

 

 

 

The events

 

 

internal class BasemapChangedEvent : PubSubEvent<Basemap>
{
}

internal class BasemapsLoadedEvent: PubSubEvent<List<AppBasemap>>
{
}

public class AppBasemap
{
	public Basemap Basemap { get; set; }
	public MapFileConfig MapFileConfig { get; set; }
}

 

 

where MapFileConfig is just a holder for paths to the maps and thumbnails.

 

And wherever you handle your Map:

 

 

internal class MainMapVM : BindableBase
{
	public MainMapVM(IEventAggregator eventAggregator)
	{
		_eventAggregator.GetEvent<BasemapChangedEvent>().Subscribe(HandleBasemapChangedEvent);
		LoadBasemaps().Await();
	}

 
	private void HandleBasemapChangedEvent(Basemap obj)
	{
		Map.Basemap = obj;
	}

	private async Task LoadBasemaps()
	{
		var appBasemaps = await _layerProvider.GetAppBasemaps(_isOnline);

		Map.Basemap = appBasemaps.First().Basemap;			
		_eventAggregator.GetEvent<BasemapsLoadedEvent>().Publish(appBasemaps);
	}

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

 

 

 

 

View solution in original post

10 Replies
williambohrmann3
Esri Contributor

Hello Viktor, we are investigating. A workaround would be to call BasemapGallery.AvailableBasemaps.Clear() which'll remove all the AGOL basemaps. Then, call BasemapGallery.AvailableBasemaps.Add(await BasemapGalleryItem.CreateAsync(basemap)) to add your custom basemap. Currently, we've found that instantiating a new List or ObservableCollection object and setting it to BasemapGallery.AvailableBasemaps will not visually update the toolkit component unless you do this twice. We should be logging this bug in the toolkit repo.

ViktorSafar
Frequent Contributor

Thanks William. I put the logic in the code behind the button click - and it kinda works. Just that my basemap becomes disabled when it loads. Any ideas why?

ViktorSafar_1-1675411966580.gif

ViktorSafar_2-1675412439551.png

 

 

 

private bool _ownBaseMapsAdded = false;
private void ButtonBasemap_Click(object sender, System.Windows.RoutedEventArgs e)
{
	if (BasemapGallery.Visibility == System.Windows.Visibility.Visible)
	{
		BasemapGallery.Visibility = System.Windows.Visibility.Hidden;
	}
	else
	{
		// workaround https://community.esri.com/t5/net-maps-sdk-questions/esritoolkit-basemapgallery-availablebasemaps/td-p/1254306
		if (!_ownBaseMapsAdded)
		{
			var existingBms = new List<BasemapGalleryItem>(BasemapGallery.AvailableBasemaps);
			BasemapGallery.AvailableBasemaps.Clear();
			BasemapGallery.AvailableBasemaps.AddRange(((MainMapVM)DataContext).AvailableBasemaps);
			BasemapGallery.AvailableBasemaps.AddRange(existingBms);
			_ownBaseMapsAdded = true;
		}

		BasemapGallery.Visibility = System.Windows.Visibility.Visible;
	}
}

 

 

 

0 Kudos
williambohrmann3
Esri Contributor

Does your MapView and custom basemap have the same spatial reference? If not, it could be violating a design requirement for BasemapGallery. Also, are you using .net 6 or .net framework?

0 Kudos
ViktorSafar
Frequent Contributor

I am on net6.

Thanks, the problem was the spatial reference of the MapView, I now instantiate the map with a specific SR

var spatialRef = SpatialReference.Create(_configuration.GetValue<int>("SpatialReference"));
Map = new Map(spatialRef); // 25833

 

I have managed to set 2 of my own maps (both with SR 25833) into the gallery. But they do not switch the basemap:

ViktorSafar_0-1675683367512.gifand 

I load the list of basemaps, then assign the 1st one to the Map.Basemap, and then set the list as AvailableBasemaps for the gallery.

var availableBasemaps = new ObservableCollection<BasemapGalleryItem>();
var appBasemaps = await _layerProvider.GetAppBasemaps(online: true);

Map.Basemap = appBasemaps.First().Basemap;

foreach (var bm in appBasemaps)
{
	var item = await BasemapGalleryItem.CreateAsync(bm.Basemap);                
    var location = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, bm.MapFileConfig.Thumbnail);
	var bytes = await File.ReadAllBytesAsync(location);
	item.Thumbnail = new Esri.ArcGISRuntime.UI.RuntimeImage(bytes);
    availableBasemaps.Add(item);
}

AvailableBasemaps = availableBasemaps;

 

where AppBasemap is just a wrapper for Esri's Basemap and a config

public class AppBasemap
{
    public Basemap Basemap { get; set; }
    public MapFileConfig MapFileConfig { get; set; }
}

 

To reiterate, the gallery is connected to the MapView in XAML:

        <esri:MapView 
            x:Name="MainMapView" 
            Map="{Binding Map}"
            />

            <esritoolkit:BasemapGallery
                x:Name="BasemapGallery"     
                GeoModel="{Binding ElementName=MainMapView, Path=Map}"                
                />

 

and the code that adds my custom basemaps to the gallery:

private bool _ownBaseMapsAdded = false;
private void ButtonBasemap_Click(object sender, System.Windows.RoutedEventArgs e)
{
	if (BasemapGallery.Visibility == System.Windows.Visibility.Visible)
	{
		BasemapGallery.Visibility = System.Windows.Visibility.Hidden;
	}
	else
	{
		// workaround https://community.esri.com/t5/net-maps-sdk-questions/esritoolkit-basemapgallery-availablebasemaps/td-p/1254306
		if (!_ownBaseMapsAdded)
		{
			BasemapGallery.AvailableBasemaps.Clear();
			BasemapGallery.AvailableBasemaps.AddRange(((MainMapVM)DataContext).AvailableBasemaps);			
			_ownBaseMapsAdded = true;
		}

		BasemapGallery.Visibility = System.Windows.Visibility.Visible;
	}
}

 

0 Kudos
williambohrmann3
Esri Contributor

Could you check if the second basemap is equal to the first basemap (not talking gallery items)? I see they have different thumbnails which indicates to me that they are different gallery items. Also, I'd try to set the names of both basemap items.

0 Kudos
ViktorSafar
Frequent Contributor

The basemaps are not equal, the basemap names are set before it reaches the foreach where it creates the gallery items:

appBasemaps[0].Basemap == appBasemaps[1].Basemap
false
appBasemaps[0].Basemap.Name
"Gråtone Vector"
appBasemaps[1].Basemap.Name
"Kyst Vector"

the basemap looks like this

bm.Basemap
{Esri.ArcGISRuntime.Mapping.Basemap}
    ApiKey: ""
    BaseLayers: {Esri.ArcGISRuntime.Mapping.LayerCollection}
    Credential: null
    Item: null
    LoadError: null
    LoadStatus: Loaded
    Name: "Gråtone Vector"
    ReferenceLayers: {Esri.ArcGISRuntime.Mapping.LayerCollection}
    Uri: null

where the BaseLayers contains 1 item:

bm.Basemap.BaseLayers.First()
{Esri.ArcGISRuntime.Mapping.ArcGISVectorTiledLayer}
    ApiKey: ""
    Attribution: "Kartverket, Geovekst, Kommuner, Corine og OSM - Geodata AS"
    CanChangeVisibility: true
    Credential: null
    Description: ""
    FullExtent: {Envelope[XMin=-4148199.2834, YMin=3127527.3873999994, XMax=3555318.5901999995, YMax=9533260.227400001, Wkid=25833]}
    Id: ""
    IsIdentifyEnabled: false
    IsVisible: true
    Item: null
    ItemResourceCache: null
    LoadError: null
    LoadStatus: Loaded
    MaxScale: 625
    MinScale: 40960000
    Name: "GeocacheGraatoneVector"
    Opacity: 1
    ShowInLegend: true
    Source: {file:///C:/temp/poc/GeocacheGraatoneVector.vtpk}
    SourceInfo: {Esri.ArcGISRuntime.ArcGISServices.VectorTileSourceInfo}
    SpatialReference: {SpatialReference[Wkid=25833]}
    Style: {Esri.ArcGISRuntime.Mapping.VectorTileStyle}
    SublayerContents: {Esri.ArcGISRuntime.Internal.SublayerContentCollection}
    VectorTileCache: {Esri.ArcGISRuntime.Mapping.VectorTileCache}

 

and the gallery item inherits the name of the basemap as soon as the item is created (the following is from the immediate window from the line availableBasemaps.Add(item); in the foreach cycle).

item
{Esri.ArcGISRuntime.Toolkit.UI.BasemapGalleryItem}
    Basemap: {Esri.ArcGISRuntime.Mapping.Basemap}
    IsLoading: false
    IsLoadingThumbnail: false
    IsValid: true
    Name: "Gråtone Vector"
    SpatialReference: null
    Thumbnail: {Esri.ArcGISRuntime.UI.RuntimeImage}
    ThumbnailData: {byte[17448]}
    Tooltip: "Gråtone Vector"
item
{Esri.ArcGISRuntime.Toolkit.UI.BasemapGalleryItem}
    Basemap: {Esri.ArcGISRuntime.Mapping.Basemap}
    IsLoading: false
    IsLoadingThumbnail: false
    IsValid: true
    Name: "Kyst Vector"
    SpatialReference: null
    Thumbnail: {Esri.ArcGISRuntime.UI.RuntimeImage}
    ThumbnailData: {byte[25556]}
    Tooltip: "Kyst Vector"

 

 

 

 

0 Kudos
williambohrmann3
Esri Contributor

Are you able to provide a smaller reproducer that demonstrates the problem? I'd like to debug and see if the IsLoading flag is set to true to see if its a LoadAsync() issue.

0 Kudos
ViktorSafar
Frequent Contributor

Here it is https://github.com/safarviktor/BasemapGallery.Bug

The 2 vector tile services that are hardcoded are open to public.

I found something potentially important. The gallery will switch the map to the 1st selected basemap after the AvailableBasemaps property of the gallery is updated, but not any subsequent ones:

ViktorSafar_0-1675885937644.gif

 

0 Kudos
williambohrmann3
Esri Contributor

Thank you Viktor for the reproducer. Upon debugging, we realized when two basemaps both have a null URI, BasemapGalleryItem.EqualsBasemap would return true. Different basemaps can both have null URI's, as in your case (despite providing the URI for the VectorTiledLayer). It should be returning false. This has been patched in .NET toolkit repo. 

As for the issue with basemap gallery items not displaying without workaround - we are unsure what is causing this: BasemapGallery.AvailableBasemaps items not displaying after object instantiation · Issue #487 · Esri...