Blog
All Blog Posts | Next Post | Previous Post
TMS.NET Maps
Today
For years, FNC Maps has given VCL and FMX developers one component that talks to Google Maps, Azure Maps, HERE, Mapbox, Leaflet and half a dozen other providers through a single, consistent API. It is one of those components people forget is doing something hard, because it never feels hard to use.
We kept getting the same question from teams who had also moved part of their stack to .NET: "can we get that in WinForms, WPF, MAUI or Blazor?"
Today we are revealing the answer: TMS.NET Maps, a full port of the FNC Maps engine to .NET, and it is now open for a closed beta.
- One control, four platforms WinForms, WPF, .NET MAUI and Blazor all share the exact same async API, so the code you write for a desktop app largely drops straight into a mobile or web one.
- Twelve mapping and location providers Google, Azure Maps, HERE, Mapbox, Leaflet/OpenStreetMap, OpenLayers, Apple MapKit, TomTom, GeoApify, OpenRouteService and IPStack, selectable at runtime.
- Batteries included markers, shapes, clustering, heatmaps, drawing tools, popups, custom overlays and companion services for geocoding, directions, elevation, places and IP location all ship in the box.
Why we built it
FNC Maps exists because Delphi developers should not have to learn a different mapping SDK for every provider, and should not have to rewrite their map code when they move a project from VCL to FMX, or from desktop to mobile.
The same argument holds even more strongly in .NET, where a single company might have a WinForms line-of-business app, a WPF dashboard, a MAUI field app and a Blazor customer portal, all wanting the same "show a map, drop some pins, draw a route" functionality. Nobody wants to learn four different map SDKs, four different marker models and four different event systems to do the same thing four times.
So TMS.NET Maps is not a new component with a similar name it is the same design philosophy as FNC Maps, rebuilt natively for .NET: one shared core, one control per platform, and a WebView-based rendering layer underneath so every provider's own JavaScript map engine can be used as-is, at full fidelity.
One shared core, one control per platform, every provider's own map engine underneath.
What's inside
Everything below is implemented once, in the shared core, and works identically across all four platforms.
- Overlays markers (with custom icons, drag support, drop-in animation and info windows), polylines (including geodesic lines and directional arrow symbols), polygons, rectangles and circles, all with full add/update/remove/clear APIs.
- Clustering group large marker sets into clusters automatically as the user zooms, with configurable clustering options.
- Heatmaps render weighted point data as a heat layer, independent of markers.
- Drawing tools let end users draw shapes directly on the map and get notified when they finish.
- Popups and custom overlays anchored popups for ad-hoc content, plus HTML element containers you can dock to map corners or center for custom UI (legends, search boxes, mini dashboards).
- A genuinely rich event model click, right-click, double-click, hover, drag and move events are exposed consistently for the map itself and for every overlay type (markers, polylines, polygons, circles, rectangles).
- Companion services
Geocoding,Directions,Elevation,Places,StaticMapandLocation(IP-based geolocation) ship as separate packages you can use standalone, even in a backend service with no map UI at all. A dedicatedRouteCalculatorservice builds on top of Directions for turn-by-turn, navigation-style scenarios. (More on these below.) - Runtime provider switching call one method to swap the entire map from, say, Google to Azure Maps, and your markers, lines and shapes carry over automatically.
A map in ten lines
Here is the entire "hello world" for the WPF control the WinForms, MAUI and Blazor versions of this are almost identical.
<Window x:Class="QuickStart.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:maps="clr-namespace:TMS.Maps.Wpf;assembly=TMS.Maps.UI.Wpf"
Title="TMS.NET Maps Quick Start" Height="600" Width="900">
<maps:MapControl x:Name="Map"
Provider="Google"
ApiKey="YOUR_GOOGLE_MAPS_KEY"
CenterLatitude="40.7128" CenterLongitude="-74.0060"
ZoomLevel="10"
MapReady="Map_Ready"/>
</Window>private async void Map_Ready(object? sender, EventArgs e)
{
await Map.AddMarkerAsync(new MapMarker(new Coordinate(40.7128, -74.0060), "New York")
{
Options = new MarkerOptions { Animation = MarkerAnimation.Drop }
});
await Map.FitToMarkersAsync();
}That is a fully interactive map, panned and zoomed to fit its markers, with zero JavaScript written by you.
Bonus: swap providers without losing your overlays
Because every provider implements the same shared core, switching the backing map engine at runtime is one call useful for A/B testing a provider, falling back when a key is missing, or just giving end users a choice.
// Markers, lines and shapes carry over to the new provider automatically. await Map.SwitchProviderAsync(Providers.Azure, azureMapsApiKey);
Beyond the map: the service layer
Not every scenario needs a rendered map. An ASP.NET Core API validating an address, a nightly batch job geocoding a CSV of customers, or a console tool generating a static map thumbnail for an email none of that needs a MapControl, a WebView, or a reference to any UI framework at all.
Six of the seven companion packages are plain service classes: construct one with a provider and an API key, call an async method, get a typed result back.
using var geocoding = new GeocodingService(Providers.Google, apiKey);
var result = await geocoding.GetAsync("1600 Amphitheatre Parkway, Mountain View, CA");
if (result.Success)
{
var match = result.Data.First();
Console.WriteLine($"{match.FormattedAddress} -> {match.Coordinate.Latitude}, {match.Coordinate.Longitude}");
}Directions follows the same shape, and returns distance, duration and the route geometry:
var directions = new DirectionsService(Providers.Google, apiKey);
var routes = await directions.GetAsync(
new Coordinate(52.3676, 4.9041), // Amsterdam
new Coordinate(50.8503, 4.3517)); // Brussels
var route = routes.Data.First();
Console.WriteLine($"{route.Summary}: {route.Distance / 1000:F1} km, {route.Duration / 60} min");The full lineup:
- Geocoding address to coordinates and back, via
GetAsync(string address)orGetAsync(Coordinate). Backed by Google, Azure, HERE, Mapbox, OpenRouteService and GeoApify. - Directions routes between two points or a full waypoint list, returning distance, duration, turn-by-turn steps and the route geometry. Backed by Google, Azure, HERE, Mapbox, OpenRouteService, GeoApify and TomTom.
- Elevation elevation for a single coordinate or a whole list at once, for terrain profiles. Backed by Google and OpenRouteService.
- Places search, autocomplete and place details through one
GetAsync(PlacesRequest)call. Backed by Google, Azure, HERE, Apple MapKit, GeoApify and TomTom. - StaticMap either a ready-to-use image URL (
GetUrl, synchronous, no HTTP call) or the actual image bytes as aStream(GetAsync) for emails, PDFs or thumbnails with no map control anywhere in sight. Backed by Google, Azure, HERE, Mapbox and TomTom. - Location IP-based geolocation, no GPS and no user permission prompt required. Backed by Google and IPStack.
Every one of these takes only a Providers value and an API key to construct no map control, no WebView, no reference to WinForms, WPF, MAUI or Blazor at all. They are just as at home in an ASP.NET Core service or a background job as in a UI project.
RouteCalculator: more than a thin wrapper
RouteCalculator sits a layer above Directions. Instead of a one-shot "get me a route," it models a stateful, editable route plan: build it, add waypoints, undo and redo changes, save and reload it, and optionally have it render and update itself on a live map.
var directionsService = new DirectionsService(Providers.Google, apiKey);
var routeCalculator = new RouteCalculatorService(directionsService);
var plan = await routeCalculator.CalculateRouteAsync(
"Amsterdam, Netherlands",
"Brussels, Belgium");
foreach (var segment in plan.Data!.Segments)
Console.WriteLine($"{segment.Distance / 1000:F1} km, {segment.Duration / 60} min");Pass it an IRouteRenderer the built-in MapRouteRenderer does this for a MapControl and the same plan draws and updates itself on screen as it changes. Nothing about the service itself requires that you do so, which is what makes it equally useful in a delivery-planning backend with no map in sight.
Why this matters
- One API to learn, not four the same marker, shape and event code works on desktop, mobile and web.
- No provider lock-in pick Google today, add Azure or HERE tomorrow, or offer a free no-key option with Leaflet or OpenLayers, without rewriting your map code.
- A WebView-based renderer means every provider's own map engine runs at native fidelity, not a reimplementation of it.
- Geocoding, directions, elevation, places and location services are available standalone, so backend-only projects can use them without any UI dependency at all.
- It carries forward everything FNC Maps developers already trust, now available to .NET teams.
Join the closed beta
TMS.NET Maps targets .NET 10 and is available today as a private, evaluation-only closed beta covering WinForms, WPF, MAUI and Blazor.
Sign up on the TMS .NET page and we'll get you access to the private package feed, the getting-started guide, and a direct channel to the team building it. Your feedback is exactly what will shape the 1.0 release.
TMS.NET Maps brings the same cross-platform mapping engine behind TMS FNC Maps to WinForms, WPF, .NET MAUI and Blazor one API, twelve providers, and every overlay, clustering, heatmap and routing feature you'd expect, in native .NET. Sign up for the closed beta.
Bradley Velghe
This blog post has not received any comments yet. Add a comment.
All Blog Posts | Next Post | Previous Post