ShapeFileFeatureLayer Guide¶
Namespace: ThinkGeo.Core
Assembly: ThinkGeo.Core
Availability: All ThinkGeo products (WPF, WinForms, Blazor, MAUI, Web API, etc.)
Overview¶
ShapeFileFeatureLayer is one of the most commonly used layer types in ThinkGeo. It provides a direct interface for loading, rendering, querying, and editing Esri Shapefile data (.shp format) on a ThinkGeo map. It inherits from FeatureLayer, which in turn inherits from Layer, and internally delegates all data access to a ShapeFileFeatureSource.
Because it lives in the ThinkGeo.Core namespace, ShapeFileFeatureLayer is shared across all ThinkGeo UI products. The same class, the same API, and the same code patterns apply whether you are building a WPF desktop application, a WinForms app, a Blazor server app, a MAUI mobile app, or a headless server process.
What Is a Shapefile?¶
A Shapefile is a multi-file geospatial vector data format developed by Esri. At minimum, three files are required:
| File | Purpose |
|---|---|
.shp | Geometry (points, lines, polygons) |
.shx | Shape index for fast positional access |
.dbf | Attribute data in dBASE format |
Optional but commonly present files:
| File | Purpose |
|---|---|
.prj | Coordinate reference system (WKT projection) |
.idx | ThinkGeo spatial index for performance |
.ids | ThinkGeo spatial index (companion to .idx) |
Class Hierarchy¶
Layer
└── FeatureLayer
└── ShapeFileFeatureLayer
ShapeFileFeatureLayer exposes all of the base FeatureLayer and Layer functionality, including:
ZoomLevelSet— controls styles at each zoom scaleQueryTools— spatial and attribute queriesEditTools— feature add/update/delete (read-write mode only)FeatureSource— direct access to the underlyingShapeFileFeatureSourceIsOpen,Open(),Close()— lifecycle management
Constructors¶
// Most common: load a shapefile by path
var layer = new ShapeFileFeatureLayer(@"./Data/Countries.shp");
// Specify both shapefile path and index path explicitly
var layer = new ShapeFileFeatureLayer(
@"C:\MapData\Buildings.shp",
@"C:\MapData\Buildings.idx");
Note: The
ShapeFileReadWriteModeenum from the legacyThinkGeo.MapSuiteAPI does not exist inThinkGeo.Core. Simply construct the layer with the path — no mode flag is needed to useEditTools.
Basic Usage Pattern¶
1. Minimal Setup (Read-Only Display)¶
using ThinkGeo.Core;
// Create the layer
var countriesLayer = new ShapeFileFeatureLayer(@"./Data/Countries.shp");
// Set a style (apply from ZoomLevel01 all the way to Level20)
// AreaStyle constructor takes (GeoPen outlinePen, GeoBrush fillBrush)
countriesLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
AreaStyle.CreateSimpleAreaStyle(GeoColors.LightGreen, GeoColors.Gray);
countriesLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
// Add to a LayerOverlay, then add the overlay to the map
var overlay = new LayerOverlay();
overlay.Layers.Add("countries", countriesLayer);
mapView.Overlays.Add(overlay);
2. With Projection Reprojection¶
Most shapefiles are in a local or geographic coordinate system (e.g., EPSG:4326 WGS84 or a state plane). ThinkGeo maps typically render in Spherical Mercator (EPSG:3857). Use ProjectionConverter to reproject on-the-fly:
var hotelsLayer = new ShapeFileFeatureLayer(@"./Data/Hotels.shp");
// Reproject from Texas State Plane (EPSG:2276) to Web Mercator (EPSG:3857)
hotelsLayer.FeatureSource.ProjectionConverter = new ProjectionConverter(2276, 3857);
hotelsLayer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle =
new PointStyle(PointSymbolType.Circle, 8, GeoBrushes.DarkRed, new GeoPen(GeoBrushes.White, 2));
hotelsLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
Tip: Assign
ProjectionConverterbefore the layer is opened or added to an overlay — it is read atOpen()time.
Styling¶
Styles in ThinkGeo are assigned through the ZoomLevelSet property. Each zoom level (ZoomLevel01 through ZoomLevel20) can carry its own style. The ApplyUntilZoomLevel property cascades a style across a range of zoom levels.
AreaStyle (Polygons)¶
// Fill-only (no outline):
parksLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
new AreaStyle(new GeoSolidBrush(GeoColors.PastelGreen));
// Fill + outline: constructor is AreaStyle(GeoPen outlinePen, GeoBrush fillBrush)
parksLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
new AreaStyle(new GeoPen(GeoColors.DarkGreen, 1), new GeoSolidBrush(GeoColors.PastelGreen));
// Or use the static factory helper:
parksLayer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
AreaStyle.CreateSimpleAreaStyle(GeoColors.PastelGreen, GeoColors.DarkGreen);
parksLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
LineStyle (Lines / Roads)¶
streetsLayer.ZoomLevelSet.ZoomLevel14.DefaultLineStyle =
new LineStyle(new GeoPen(GeoBrushes.Gray, 5), new GeoPen(GeoBrushes.White, 4));
streetsLayer.ZoomLevelSet.ZoomLevel14.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level16;
PointStyle (Points)¶
hotelsLayer.ZoomLevelSet.ZoomLevel12.DefaultPointStyle =
new PointStyle(PointSymbolType.Circle, 4, GeoBrushes.DarkRed, new GeoPen(GeoBrushes.White, 2));
hotelsLayer.ZoomLevelSet.ZoomLevel12.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level13;
TextStyle (Labels)¶
hotelsLayer.ZoomLevelSet.ZoomLevel16.DefaultTextStyle =
new TextStyle("NAME", new GeoFont("Segoe UI", 12, DrawingFontStyles.Bold), GeoBrushes.DarkRed)
{
TextPlacement = TextPlacement.Lower,
YOffsetInPixel = 4,
HaloPen = new GeoPen(GeoBrushes.White, 2),
DrawingLevel = DrawingLevel.LabelLevel,
AllowLineCarriage = true
};
hotelsLayer.ZoomLevelSet.ZoomLevel16.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
Scale-Dependent Rendering (Different Styles at Different Zoom Levels)¶
A powerful pattern is to progressively add detail as the user zooms in:
// Only show outline at low zoom
streetsLayer.ZoomLevelSet.ZoomLevel10.DefaultLineStyle =
new LineStyle(new GeoPen(GeoBrushes.LightGray, 1));
streetsLayer.ZoomLevelSet.ZoomLevel10.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level11;
// Thicker + styled casing at mid zoom
streetsLayer.ZoomLevelSet.ZoomLevel12.DefaultLineStyle =
new LineStyle(new GeoPen(GeoBrushes.LightGray, 2), new GeoPen(GeoBrushes.White, 1));
// Add labels at high zoom
streetsLayer.ZoomLevelSet.ZoomLevel16.DefaultTextStyle =
new TextStyle("FULL_NAME", new GeoFont("Segoe UI", 9, DrawingFontStyles.Bold), GeoBrushes.Black)
{
SplineType = SplineType.StandardSplining,
HaloPen = new GeoPen(GeoBrushes.White, 2),
DrawingLevel = DrawingLevel.LabelLevel
};
streetsLayer.ZoomLevelSet.ZoomLevel16.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
ValueStyle (Data-Driven Styling)¶
Apply different styles based on an attribute column value:
// Use object initializer syntax to set ColumnName
var valueStyle = new ValueStyle
{
ColumnName = "CATEGORY"
};
// ValueItem(matchValue, style) — AreaStyle(GeoPen outlinePen, GeoBrush fillBrush)
valueStyle.ValueItems.Add(new ValueItem("Park",
new AreaStyle(new GeoPen(GeoColors.DarkGreen, 1), new GeoSolidBrush(GeoColors.PastelGreen))));
valueStyle.ValueItems.Add(new ValueItem("Hospital",
new AreaStyle(new GeoPen(GeoColors.DarkRed, 1), new GeoSolidBrush(GeoColors.LightCoral))));
valueStyle.ValueItems.Add(new ValueItem("School",
new AreaStyle(new GeoPen(GeoColors.DarkGoldenrod, 1), new GeoSolidBrush(GeoColors.LightYellow))));
layer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(valueStyle);
layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
Querying Features¶
The QueryTools property exposes spatial and attribute query methods. Always Open() the layer before querying, and Close() it when done.
layer.Open();
// Get all features (avoid on large datasets)
Collection<Feature> all =
layer.QueryTools.GetAllFeatures(ReturningColumnsType.AllColumns);
// Query by bounding box
RectangleShape extent = new RectangleShape(-97.5, 30.5, -97.0, 31.0);
Collection<Feature> inBox =
layer.QueryTools.GetFeaturesInsideBoundingBox(extent, ReturningColumnsType.AllColumns);
// Query by column value
Collection<Feature> byValue =
layer.QueryTools.GetFeaturesByColumnValue("STATE_NAME", "Texas",
ReturningColumnsType.AllColumns);
// Spatial: find the N features nearest to a point (within optional max distance)
PointShape clickPoint = new PointShape(-97.2, 30.7);
Collection<Feature> nearest =
layer.QueryTools.GetFeaturesNearestTo(
clickPoint,
GeographyUnit.DecimalDegree,
5,
new Collection<string>(), // returning column names
100, // max distance
DistanceUnit.Meter); // distance unit
layer.Close();
Accessing Feature Attributes¶
Each Feature has a ColumnValues dictionary:
foreach (Feature feature in features)
{
string name = feature.ColumnValues["NAME"];
string population = feature.ColumnValues["POPULATION"];
Console.WriteLine($"{name}: {population}");
}
Editing Features (Read-Write Mode)¶
To add, update, or delete features, open the layer and use EditTools within a transaction:
Adding a Feature¶
var layer = new ShapeFileFeatureLayer("./Data/Points.shp");
layer.Open();
layer.EditTools.BeginTransaction();
var newPoint = new PointShape(-97.3, 30.6);
var newFeature = new Feature(newPoint);
newFeature.ColumnValues["NAME"] = "New Location";
layer.EditTools.Add(newFeature);
layer.EditTools.CommitTransaction();
layer.Close();
Deleting a Feature¶
layer.Open();
layer.EditTools.BeginTransaction();
Collection<Feature> toDelete =
layer.QueryTools.GetFeaturesByColumnValue("IDOBJ", "42",
ReturningColumnsType.NoColumns);
if (toDelete.Count > 0)
{
layer.EditTools.Delete(toDelete[0].Id);
}
layer.EditTools.CommitTransaction();
layer.Close();
Updating Feature Columns (Schema Changes)¶
Note: Column schema mutations (adding, renaming, or deleting columns on an existing shapefile) are not covered by the ThinkGeo HowDoI samples. The standard approach is to create a new shapefile with the desired schema using
ShapeFileFeatureLayer.CreateShapeFile(), copy features into it, and replace the original file. This avoids the DBF format's limited schema-mutation support.
If you need to add a column and populate it, the recommended pattern is:
// 1. Create a new shapefile with the updated column schema
var newColumns = new Collection<DbfColumn>
{
new DbfColumn("NAME", DbfColumnType.String, 255, 0),
new DbfColumn("PRIORITY", DbfColumnType.Numeric, 10, 0) // new column
};
ShapeFileFeatureLayer.CreateShapeFile(
ShapeFileType.Point, "./Data/Points_new.shp", newColumns,
System.Text.Encoding.UTF8, OverwriteMode.Overwrite);
// 2. Copy features from the old layer into the new one
var oldLayer = new ShapeFileFeatureLayer("./Data/Points.shp");
var newLayer = new ShapeFileFeatureLayer("./Data/Points_new.shp");
oldLayer.Open();
newLayer.Open();
newLayer.EditTools.BeginTransaction();
foreach (var feature in oldLayer.QueryTools.GetAllFeatures(ReturningColumnsType.AllColumns))
{
// Carry over existing column values; new column defaults to empty
newLayer.EditTools.Add(feature);
}
newLayer.EditTools.CommitTransaction();
oldLayer.Close();
newLayer.Close();
Creating a New Shapefile¶
Use the static ShapeFileFeatureLayer.CreateShapeFile() method to create a brand-new shapefile:
var columns = new Collection<DbfColumn>
{
new DbfColumn("NAME", DbfColumnType.String, 255, 0),
new DbfColumn("CATEGORY", DbfColumnType.String, 50, 0),
new DbfColumn("VALUE", DbfColumnType.Numeric, 10, 2)
};
ShapeFileFeatureLayer.CreateShapeFile(
ShapeFileType.Point,
"./Data/NewPoints.shp",
columns,
System.Text.Encoding.UTF8,
OverwriteMode.Overwrite);
ShapeFileType Values¶
| Value | Description |
|---|---|
Point | Single point features |
Multipoint | Collections of points |
Polyline | Line / polyline features |
Polygon | Area / polygon features |
NullShape | Null/empty geometry |
Building a Spatial Index¶
For large shapefiles, a ThinkGeo spatial index (.idx / .ids) dramatically improves rendering and query performance by avoiding full file scans:
// Build the index once (typically at app startup or as a build step).
// Can be called on either ShapeFileFeatureLayer or ShapeFileFeatureSource.
ShapeFileFeatureLayer.BuildIndexFile(
"./Data/LargeLayer.shp",
BuildIndexMode.Rebuild); // Rebuilds even if index already exists
// Use DoNotRebuild to skip the build if the index file is already present:
ShapeFileFeatureLayer.BuildIndexFile(
"./Data/LargeLayer.shp",
BuildIndexMode.DoNotRebuild);
Tip: Run
BuildIndexFileduring a setup/installation step rather than at runtime. Index building on a very large shapefile can take seconds to minutes.
Getting the Bounding Box¶
layer.Open();
RectangleShape bbox = layer.GetBoundingBox();
layer.Close();
// Use it to set the initial map extent
mapView.CurrentExtent = bbox;
Projection and Coordinate Systems¶
If your shapefile has a .prj file, ThinkGeo will read the projection automatically. However, you must still set the ProjectionConverter if the map is rendering in a different coordinate system.
Use ProjectionConverter with EPSG codes — the standard approach for all ThinkGeo projections:
// Create a new ProjectionConverter to convert between the shapefile's CRS and the map's CRS.
// Constructor: ProjectionConverter(internalEpsg, externalEpsg)
// "internal" = the projection the data is stored in (the shapefile's native CRS)
// "external" = the projection the map is displayed in (typically Spherical Mercator, 3857)
// Example: WGS84 (4326) shapefile on a Web Mercator (3857) map
var projectionConverter = new ProjectionConverter(4326, 3857);
layer.FeatureSource.ProjectionConverter = projectionConverter;
// Example: Texas State Plane North Central (2276) shapefile on a Web Mercator (3857) map
var projectionConverter = new ProjectionConverter(2276, 3857);
layer.FeatureSource.ProjectionConverter = projectionConverter;
Advanced: Custom FeatureSource (Performance Filtering)¶
For very large shapefiles (e.g., 800k+ features), you can subclass ShapeFileFeatureSource and override drawing methods to filter out features that are too small to be visible at the current scale:
public class FilteredShapeFileFeatureSource : ShapeFileFeatureSource
{
public double MinDrawingPixelSize { get; set; } = 2.0;
public FilteredShapeFileFeatureSource(string shapePathFilename)
{
ShapePathFileName = shapePathFilename;
}
protected override Collection<string> GetFeatureIdsForDrawingCore(
RectangleShape boundingBox, double screenWidth, double screenHeight)
{
Collection<string> allIds =
base.GetFeatureIdsForDrawingCore(boundingBox, screenWidth, screenHeight);
double resolutionX = boundingBox.Width / screenWidth;
double resolutionY = boundingBox.Height / screenHeight;
var filteredIds = new Collection<string>();
foreach (string id in allIds)
{
RectangleShape featureBbox = GetBoundingBoxById(id);
double drawingWidth = featureBbox.Width / resolutionX;
double drawingHeight = featureBbox.Height / resolutionY;
if (Math.Max(drawingWidth, drawingHeight) > MinDrawingPixelSize)
{
filteredIds.Add(id);
}
}
return filteredIds;
}
}
// Companion custom layer
public class FilteredShapeFileFeatureLayer : ShapeFileFeatureLayer
{
public FilteredShapeFileFeatureLayer(string shapePathFilename)
{
FeatureSource = new FilteredShapeFileFeatureSource(shapePathFilename);
}
}
// Usage
var layer = new FilteredShapeFileFeatureLayer("./Data/LargePolygons.shp");
((FilteredShapeFileFeatureSource)layer.FeatureSource).MinDrawingPixelSize = 3.0;
Common Patterns and Best Practices¶
Always Pair Open() with Close()¶
layer.Open();
try
{
// work with layer
}
finally
{
layer.Close();
}
When a layer is added to a LayerOverlay on the map, ThinkGeo manages open/close automatically during rendering. You only need to manually call Open() / Close() when querying or editing outside of the rendering pipeline.
Use ReturningColumnsType to Limit Data Read¶
Fetching all columns from a large DBF is expensive. Only request what you need:
// Fetch only specific columns
var columnNames = new Collection<string> { "NAME", "POPULATION" };
Collection<Feature> features =
layer.QueryTools.GetAllFeatures(columnNames);
// Or use the enum for common cases
Collection<Feature> noAttr =
layer.QueryTools.GetAllFeatures(ReturningColumnsType.NoColumns);
Use Tile Caching for Static Layers¶
If a layer's data does not change at runtime, enable FileRasterTileCache to avoid re-rendering tiles on every map interaction:
var overlay = new LayerOverlay();
overlay.TileCache = new FileRasterTileCache(@"./TileCache", "countries_layer");
overlay.Layers.Add(countriesLayer);
Common Pitfalls and Gotchas¶
1. Missing .shx or .dbf File¶
ShapeFileFeatureLayer requires the .shp, .shx, and .dbf files to all be present in the same directory. A FileNotFoundException or unexpected empty map is often caused by deploying only the .shp.
2. Forgetting to Set ApplyUntilZoomLevel¶
If you set a style on ZoomLevel01 but do not set ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20, the style will only render at zoom level 1. At all other zoom levels the layer will appear empty or unstyled. This is one of the most common beginner mistakes.
// WRONG – only visible at zoom level 1
layer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
AreaStyle.CreateSimpleAreaStyle(GeoColors.YellowGreen, GeoColors.Black);
// CORRECT – visible at all zoom levels
layer.ZoomLevelSet.ZoomLevel01.DefaultAreaStyle =
AreaStyle.CreateSimpleAreaStyle(GeoColors.YellowGreen, GeoColors.Black);
layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
3. Projection Mismatch (Blank or Offset Map)¶
If features render in the wrong location or far off-screen, the most likely cause is a missing or incorrect ProjectionConverter. Always match the layer's native projection to the map's display projection.
4. Editing a Shapefile That Is Locked by Another Process¶
If another process (or another part of your application) has the shapefile open for writing, calling EditTools.BeginTransaction() can throw an IOException. Ensure no other code has an exclusive write lock on the .shp or .dbf files before editing.
// Correct pattern: open, transact, commit, close
layer.Open();
layer.EditTools.BeginTransaction();
// ... add / update / delete ...
layer.EditTools.CommitTransaction();
layer.Close();
5. GetBoundingBox() Returns Wrong Values After Deletes¶
When you delete features from a shapefile and then call GetBoundingBox(), the cached bounding box in the .shp header can contain stale values that do not reflect the deletion. Workaround: manually compute the bounding box from the remaining features:
layer.Open();
Collection<Feature> remaining =
layer.FeatureSource.GetAllFeatures(ReturningColumnsType.NoColumns);
RectangleShape bbox = remaining.Count > 0 ? remaining[0].GetBoundingBox() : new RectangleShape();
foreach (Feature f in remaining)
bbox.ExpandToInclude(f.GetBoundingBox());
layer.Close();
6. Multi-Thread Access Conflicts¶
In older ThinkGeo versions, opening the same shapefile from two threads simultaneously could throw an IOException ("file is being used by another process"). This was fixed in version 7.0.0.119+. In modern ThinkGeo.Core, multiple instances reading the same file in parallel are safe.
In modern versions, multiple read-only instances of the same file can be opened in parallel safely.
7. Performance Degradation on Large DBF Files¶
Rendering features that require attribute lookup (for TextStyle labels or ValueStyle coloring) requires reading from the .dbf file for each visible feature. On large datasets (hundreds of thousands of features), this can be extremely slow — even when only a few hundred features are in the current view. Solutions:
- Use the
FilteredShapeFileFeatureSourcepattern above to reduce the number of features fetched. - Minimize the number of columns fetched via
ReturningColumnsTypeor explicit column name lists. - Consider converting to a database-backed
FeatureSource(e.g., SQLite) for large datasets. - Enable tile caching to avoid re-rendering unchanged tiles.
8. Styles Must Be Set Before Rendering¶
Styles are applied at draw time. Setting a style after the map has already rendered will not be visible until the next map refresh. Always configure styles before adding the overlay to the map, or call mapView.RefreshAsync() after changing styles.
9. ShapeFileType Must Match the Data¶
When creating a new shapefile with CreateShapeFile, the ShapeFileType must exactly match the geometry you intend to write. Writing PointShape features to a Polygon shapefile will throw an exception or produce corrupt data.
Key Properties Reference¶
| Property | Type | Description |
|---|---|---|
ShapePathFileName | string | Full path to the .shp file |
IndexPathFileName | string | Full path to the spatial index .idx file |
| FeatureSource | ShapeFileFeatureSource | Direct access to the underlying feature source | | ZoomLevelSet | ZoomLevelSet | Holds ZoomLevel01–ZoomLevel20 for style assignment | | QueryTools | QueryTools | Spatial and attribute query methods | | EditTools | EditTools | Add/update/delete features (use within a transaction) | | IsOpen | bool | Whether the layer is currently open | | Name | string | Optional identifier for use in LayerOverlay.Layers dictionary | | IsVisible | bool | Controls whether the layer renders | | Transparency | float | 0 (opaque) to 1 (transparent) overlay transparency |
Key Static Methods¶
| Method | Description |
|---|---|
CreateShapeFile(...) | Creates a new empty shapefile on disk |
BuildIndexFile(...) | Builds a ThinkGeo spatial index for a shapefile |
RebuildIndexFile(...) | Rebuilds an existing spatial index |
Related Classes¶
| Class | Description |
|---|---|
ShapeFileFeatureSource | The underlying data access object; can be subclassed |
FeatureLayer | Base class providing QueryTools, EditTools, ZoomLevelSet |
LayerOverlay | Container that holds one or more layers for rendering |
ProjectionConverter | Reprojects coordinates using EPSG codes |
FileRasterTileCache | Caches rendered tiles to disk for performance |
ZoomLevelSet | Manages styles per zoom level |
Feature | Represents a single geographic feature with geometry + attributes |
DbfColumn | Defines a column in a DBF attribute table |