This document is intended for developers and maintainers of the GeoBlazor source code. The guidelines are for new development and refactoring of existing code, but may not be adhered to by all existing code.
The TypeScript files are compiled as part of the build process using a source generator and ESBuild.
The source generator, ESBuildLauncher.cs, watches the Scripts folder for changes, and then runs the ESBuild
compiler on the TypeScript files. The output is placed in the wwwroot/js folder.
If you are making changes to the TypeScript files, you can run the ESBuild compiler manually by running
src/dymaptic.GeoBlazor.Core/esBuild.ps1. You do not necessarily need to restart the .NET application
for every change, unless you see an issue after refreshing the browser with the cache disabled.
This is a known issue with the current setup of GeoBlazor. It is a conflict between the ESBuild compilation and the MSBuild static file analysis (trying to read the JS that results from ESBuild). Unfortunately, it does not matter where in the build process we kick off the ESBuild compilation, it will routinely cause a build error when building any project that references Core with a project reference. We have logged this with Microsoft here.
- Support end user use of ArcGIS tools without having to write JavaScript
- Support components as both Razor markup elements and C# code object
- Support idiomatic C# coding patterns
- Maintain a close relationship between ArcGIS and GeoBlazor architecture to allow easier maintenance, updates, and documentation
- Automate and standardize patterns whenever possible
- Simplify, minimize, and improve performance whenever possible
- We use a proprietary code generation tool to generate many of the C# and TypeScript files in the GeoBlazor project.
You will see
Component.gb.cs,Component.cs,component.gb.ts, andcomponent.tsfiles for most components. - Changes made to
.gb.csand.gb.tsfiles will be overwritten by the code generation tool and are not safe. Instead make changes to the.csand.tsfiles, which will be preserved. - You can move code from the generated (
.gb.x) file and into the non-generated file to edit it. In C#, you should mark all types, properties, and methods that you move with[CodeGenerationIgnore]to prevent the code generation tool from overwriting your changes. In TypeScript, within the "wrapper" classes, you do not need to mark the methods or property getters/setters, just move them and the generator will see that they are now in the root.tsfile. - The TypeScript
buildJs...andbuildDotNet...functions always start with an editable version that then typically calls the generatedbuildJsGenerated...version. This allows you to add custom logic to the editable version, while still allowing the code generation tool to update the generated version without losing your changes. However, if you don't want to call the generated version at all, you can simply remove the call to it in the editable version, and the generator will stop generating this function. - When adding a new component, it is best to contact the GeoBlazor team and have them start with the code generation tool. This will ensure that the generated files are created correctly, and that the code generation tool is aware of the new component. Then you can make changes and add functionality as needed.
- When overriding a generated method, in addition to moving and annotating the logic, it is a good idea to look through the code base for other instances of the same logic, so that we can either update the code generator to handle them all, or update them all manually.
Blazor components are built around the concept of automatic binding and updating when C# properties and variables change.
Each time Blazor detects a change, it will re-render the component and all of its child-components, and then update the client html.
The Blazor component lifecycle method OnAfterRender and OnAfterRenderAsync can thus be triggered many times during a
component's lifetime, and are usually called at least twice on startup. Similarly, the method OnParametersSet and
OnParametersSetAsync will be called every time Blazor detects a change in a component parameter (which also then triggers a render).
Unlike normal Blazor components, however, GeoBlazor components are not being rendered to HTML. Instead, they are being passed as data to a JavaScript layer, where they are used to generate the relevant ArcGIS objects. As you can imagine, calling the same JavaScript to render in ArcGIS repeatedly would be a bad idea. Therefore, GeoBlazor has checks in place to guarantee that a map is only rendered once, and then on actual component changes. This creates a few limitations and new capabilities that normal Blazor components do not have.
- Synchronous property setters will not automatically cause a map to update. This is because all JavaScript calls (at
least those that support all Blazor modes) are asynchronous, and there is no safe way to trigger an async method from
a synchronous setter. Instead, the suggested pattern is to implement asynchronous
Set{PropertyName}()methods that can call a custom JS function to update the value. - Similarly, property getters do not automatically load the latest value from JavaScript. If the property is expected to
be updated regularly and "read", you should implement an asynchronous
Get{PropertyName}()method to load the value from JavaScript. - Some Blazor parameters are only read once on the first render, and will not auto-bind to value changes like other components.
We do try to auto-generate code that will serialize and bind these properties back after the initial render, but this
is not always possible. If a property is not hydrated automatically, users will have to be guided to use the async
Setmethods to update properties after initial render. - We do try to support binding in the top-level
MapViewparameters such asLatitude,Longitude, andZoom. However, once a user zooms or scrolls the map manually, we cannot support the binding anymore, because the map would always "snap" back to its original position, so the binding stops being read after manual map manipulation. - GeoBlazor components must be nested in their proper parent components. Methods like
RegisterChildComponent,UnRegisterChildComponent, andValidateRequiredChildrenshould be implemented for each child component, and[RequiredParameter]should be used to enforce a parameter that needs to be set by the user. This approach should provide clear error messages if the user places a component in the wrong place or forgets a parameter. - All GeoBlazor MapComponents must have a parameterless constructor with the
[ActivatorUtilitiesConstructor]attribute. This is required for Blazor to be able to instantiate the component from Razor markup. The parameterless constructor should not set any properties, and should only be used for Razor markup. All properties should be set in a second constructor that takes all public properties as parameters, and uses#pragma warning disable BL0005to silence the warning about setting parameters outside of the component. This second constructor should also set the value ofAllowRendertofalseto prevent Blazor attempting to use the component as a razor component if it was created in code.
- Unlike other Blazor components. GeoBlazor MapComponents can be created in C# code as well as in markup. To avoid the
BL0005:Component parameter should not be set outside of its componentwarning, we create two constructors for each component, one empty constructor for razor markup, and another one that sets all properties and uses#pragma warning disable BL0005to silence the warning.
- A
MapComponentis a Razor (aka Blazor) Component that can be declared in Razor markup. For example,MapView,Map,FeatureLayer, andGraphicare allMapComponents. MapComponents are usually 1-1 with ArcGIS JavaScript classes.MapComponents, with a few top-level exceptions (e.g.,MapView,Map), can also be declared in C# code.- There should always be two constructors, an empty one for razor, and one that sets all public properties that should be used when creating the component in C#.
- Properties should never be set in C# after the component has been rendered to JavaScript.
The following patterns should be followed and enforced as much as possible:
- Define the property in razor markup, either as a
Parameteror as a child component - Use the constructor to set all properties
- Add an async
Set{PropertyName}method, which calls to JavaScript to update the value (this is not needed for every property, just commonly updated ones)
- Define the property in razor markup, either as a
- Properties with simple/primitive types (e.g. int, double, string, bool, enum) should be marked as
[Parameter]s for Blazor. This will also generate a warning if a user tries to set these properties by hand in code after the component is rendered. - Properties with object types should also inherit from
MapComponent, so they can be written as child tags in razor markup. - MapComponent properties should have
{ get; private set; }to force folks to use one of the patterns described above. - If there are any
MapComponentproperties,RegisterChildComponent,UnregisterChildComponent, andValidateRequiredChildredshould be implemented and handle all childMapComponentproperties
- All
enumtypes should be marked with the[JsonConverter(typeof(EnumToKebabCaseStringConverter<{YourType}>))]attribute - Inherited Types sometimes require custom
JsonConverterimplementations to avoid serializing as the base type. SeeLayerConverteras an example. - If you want to avoid passing
nullproperties, mark properties with[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]. However, this will also be dealt with in the next section aboutConverting GeoBlazor Objects to ArcGIS.
- Simple/primitive values can be easily copied from a C# object to an ArcGIS object. However, complex objects tend to fail, for several reasons outlined below.
- Setting a value in an ArcGIS object to
nullwhen it expects a value can cause it to fail silently. There are several patterns to avoid this.- If setting the value in a constructor, use
?? undefinedto avoid setting the value tonull.
let template = new PopupTemplate({ title: popupTemplateObject.title ?? undefined, content: content ?? undefined, outFields: popupTemplateObject.outFields ?? undefined, overwriteActions: popupTemplateObject.overwriteActions ?? false, returnGeometry: popupTemplateObject.returnGeometry ?? false });
- Check the for
undefinedandnullbefore setting the value.
if (popupTemplateObject.title !== undefined && popupTemplateObject.title !== null) { template.title = popupTemplateObject.title; }
- Several of the TypeScript files have a shorthand function for this called
hasValue.
if (hasValue(popupTemplateObject.title)) { template.title = popupTemplateObject.title; }
- If setting the value in a constructor, use
- The GeoBlazor C# types are serialized with a
Guididproperty. This is used to store and find references between the two languages. However, this may also be causing some issues when simply "setting" a dotnet object to an ArcGIS object. ArcGIS tries to auto-cast objects, but unknown properties could cause it to fail. - The suggested pattern for creating a complex ArcGIS object is to define a
buildJs...function injsBuilder.ts. This gives you a place to implement the null checks and other conversion logic necessary. You may have to use several nestedbuildJs...functions.
- While it is not as common, there are times when you need to convert an ArcGIS object to a GeoBlazor object. This is usually done when an event is fired from the JavaScript side, and you need to pass the object back to C#.
- The JsonSerializer may handle the ArcGIS object correctly
- If not, you can define a
buildDotNet...function indotNetBuilder.tsto do any necessary conversions.
- In order to call methods on an ArcGIS object, you need a reference to the JS object. However, as described above, we
often need a "shim" layer in which to cast arguments from a GeoBlazor object to an ArcGIS object. For objects like
LayersandWidgets, this is often done by providing awrapper. This is a separate TypeScript class file that has a reference to the original JS object, and replicates the method calls while allows for building custom types. SeefeatureLayerWrapper.tsfor an example. - If an object has no functions that we want to support, or if the functions only take primitive types, you do not really need a wrapper.
- Identify a feature in the ArcGIS JavaScript SDK that you want to support in GeoBlazor
- If there is no related gissue, create one and assign it to yourself
- If this is a completely new component, layer, or widget, contact the GeoBlazor team in the gissue to discuss code generation before starting work.
- Create a new branch for your work with the pattern
feature/{gissue-number}_{short-description} - Find a relevant sample from the ArcGIS JavaScript SDK, if one exists
- Create a new sample page in
dymaptic.GeoBlazor.Core.Samples.Shared/Pages. Use the same header pattern as other samples- The links in the sample header should point to a) the JS sample page and b) the data source (e.g., feature service) used, if available
- Implement the necessary C# classes and methods to support the feature
- Implement TypeScript functions and classes if necessary for your C# classes to call and use. See the
WidgetsandLayersdetails below for more examples - If possible, flesh out all properties and methods from ArcGIS classes in your C# implementation, even if they are not directly used in the sample
- Write unit tests in
dymaptic.GeoBlazor.Core.Tests.Blazor.Sharedto test your new classes and methods
- Find
Enums/LayerType.csand add a newLayerTypeenum value that matches the layer type in ArcGIS. Ensure that the name is in PascalCase and will convert to kebab-case correctly in JavaScript. You can override the default conversion with theLayerTypeConverterin the same file. - Create a new C# class in the
dymaptic.GeoBlazor.Core/Components/Layersfolder that inherits fromLayer. - Implement the
LayerTypeproperty with the new value you created on the enum./// <inheritdoc /> public override LayerType LayerType => LayerType.YourLayerType;
- Add your new layer type to
Serialization/LayerConverter.csfor de-serialization. - Implement all properties from the ArcGIS Layer class. Use the
MapComponentsrules outlined above. - Add your layer to the switch statements in
Scripts/layer.ts. - Create a
yourLayer.tsfile in theScripts/Layersfolder, and implement theyourLayerWrapperclass andbuildJsYourLayerandbuildDotNetYourLayerfunctions, using other layers as examples. If source generation is available, reference the generated file. - Create a new Layer samples page in
dymaptic.GeoBlazor.Core.Samples.Shared/Pages. Also add to theNavMenu.razor. - Create a new test class and at least one test in
dymaptic.GeoBlazor.Core.Tests.Blazor.Shared/Components.
- Find
Enums/WidgetType.csand add a newWidgetTypeenum value that matches the widget type in ArcGIS. Ensure that the name is in PascalCase and will convert to kebab-case correctly in JavaScript. - Create a new C# class in the
dymaptic.GeoBlazor.Core/Components/Widgetsfolder that inherits fromWidget. - Implement the
WidgetTypeproperty, using the new value you created on the enum./// <inheritdoc /> public override WidgetType WidgetType => WidgetType.YourWidgetType;
- Add your widget to the
Serialization/WidgetConverter.csfor de-serialization. - Implement all properties from the ArcGIS Widget class. Use the
MapComponentsrules outlined above. - Add your widget to the switch statements in
Scripts/widget.ts. - Create a
yourWidget.tsfile in theScripts/Widgetsfolder, and implement theyourWidgetWrapperclass andbuildJsYourWidgetandbuildDotNetYourWidgetfunctions, using other widgets as examples. If source generation is available, reference the generated file. - Create a new Widget samples page in
dymaptic.GeoBlazor.Core.Samples.Shared/Pages. Also add to theNavMenu.razor. - Alternatively, for simple widgets, you can add them to the
Widgets.razorsample. - Create a new test class and at least one test in
dymaptic.GeoBlazor.Core.Tests.Blazor.Shared/Components.