diff --git a/Directory.Build.props b/Directory.Build.props
index 698713f3b..b13101a72 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,7 +6,7 @@
enable
enable
true
- 4.5.3
+ 4.6.0
Debug;Release;SourceGen Highlighting
AnyCPU
true
diff --git a/build-tools/build-scripts/GBTest.cs b/build-tools/build-scripts/GBTest.cs
index 32fd6cce7..0b8451d96 100644
--- a/build-tools/build-scripts/GBTest.cs
+++ b/build-tools/build-scripts/GBTest.cs
@@ -162,14 +162,11 @@ await RunDotnetCommandWithOutputAsync(testProjectDir, "run", buildArgs, environm
double passedPercentage = (double)progress.Passed / ran * 100;
Console.WriteLine($"PASSED TESTS: {progress.Passed} / {ran} TESTS RAN ({passedPercentage:F2}%).");
Console.WriteLine($"FAILED: {progress.Failed} / SKIPPED: {progress.Skipped}");
-
- if (passedPercentage < percentage)
- {
- Console.WriteLine($"TEST RUN FAILED: Passed percentage {passedPercentage:F2}% is below the required {
- percentage}%.");
- failed = true;
- }
}
+
+ // A cancelled run never completed, so it can never be reported as a pass.
+ Console.WriteLine("TEST RUN FAILED: Test run was cancelled before completion.");
+ failed = true;
}
else
{
@@ -178,35 +175,56 @@ await RunDotnetCommandWithOutputAsync(testProjectDir, "run", buildArgs, environm
Regex finalCountRegex = new(@"^.*FINAL_SUMMARY: PASSED TESTS: (?\d+) / (?\d+)\s*$");
+ bool summaryFound = false;
int total = 0;
int passed = 0;
- foreach (string line in await File.ReadAllLinesAsync(testOutputLogPath))
+
+ if (File.Exists(testOutputLogPath))
{
- if (line.Contains("FINAL_SUMMARY"))
+ foreach (string line in await File.ReadAllLinesAsync(testOutputLogPath))
{
- string content = line.Substring(38); // 38 is the timestamp plus FINAL_SUMMARY:
-
- if (finalCountRegex.Match(line) is { Success: true } match)
- {
- total = int.Parse(match.Groups["total"].Value);
- passed = int.Parse(match.Groups["passed"].Value);
- }
- else
+ if (line.Contains("FINAL_SUMMARY"))
{
- Console.WriteLine(content);
+ string content = line.Substring(38); // 38 is the timestamp plus FINAL_SUMMARY:
+
+ if (finalCountRegex.Match(line) is { Success: true } match)
+ {
+ total = int.Parse(match.Groups["total"].Value);
+ passed = int.Parse(match.Groups["passed"].Value);
+ summaryFound = true;
+ }
+ else
+ {
+ Console.WriteLine(content);
+ }
}
}
}
- double passedPercentage = total > 0 ? (double)passed / total * 100 : 100;
- Console.WriteLine($"PASSED TESTS: {passed} / {total} TESTS PASSED ({passedPercentage:F2}%).");
-
- if (passedPercentage < percentage)
+ if (!summaryFound || total == 0)
{
- Console.WriteLine($"TEST RUN FAILED: Passed percentage {passedPercentage
- :F2}% is below the required {percentage}%.");
+ // No completed-test summary was produced. This happens when the run aborts before any
+ // test executes - e.g. AssemblyInitialize throws because the test web app never became
+ // reachable, so AssemblyCleanup (which writes the FINAL_SUMMARY) never runs. Previously
+ // this was reported as 100% passing because the percentage defaulted to 100 when total
+ // was 0, masking a hard failure as a green run.
+ Console.WriteLine("TEST RUN FAILED: No completed test results were recorded. The test run "
+ + "aborted before any tests ran (for example, AssemblyInitialize failed or the test web "
+ + "app was not reachable). Review the log output above for the root cause.");
failed = true;
}
+ else
+ {
+ double passedPercentage = (double)passed / total * 100;
+ Console.WriteLine($"PASSED TESTS: {passed} / {total} TESTS PASSED ({passedPercentage:F2}%).");
+
+ if (passedPercentage < percentage)
+ {
+ Console.WriteLine($"TEST RUN FAILED: Passed percentage {passedPercentage
+ :F2}% is below the required {percentage}%.");
+ failed = true;
+ }
+ }
}
if (failed)
diff --git a/build-tools/build-scripts/ScriptBuilder.cs b/build-tools/build-scripts/ScriptBuilder.cs
index c6bf79787..5a8ea1c99 100644
--- a/build-tools/build-scripts/ScriptBuilder.cs
+++ b/build-tools/build-scripts/ScriptBuilder.cs
@@ -32,6 +32,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
bool excludeMode = false;
@@ -424,10 +425,58 @@ static async Task BuildScript(string scriptName, string scriptsDir, string
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken);
+
+ // The file-based-app SDK bakes the source .cs file's absolute path into the generated
+ // runtimeconfig.json. Rewrite it to a stable relative value so the committed output doesn't churn across machines.
+ if (process.ExitCode == 0 && scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
+ {
+ RewriteRuntimeConfigPaths(scriptName, outDir);
+ }
+
return process.ExitCode;
}
-static async Task CleanScript(string scriptName, string scriptsDir, string outDir, string runtime,
+///
+/// Replaces the absolute EntryPointFilePath/EntryPointFileDirectoryPath that the file-based-app SDK
+/// writes into a script's runtimeconfig.json with machine-independent relative values.
+///
+static void RewriteRuntimeConfigPaths(string scriptName, string outDir)
+{
+ string runtimeConfigPath = Path.Combine(outDir, Path.GetFileNameWithoutExtension(scriptName) + ".runtimeconfig.json");
+ if (!File.Exists(runtimeConfigPath))
+ {
+ return;
+ }
+
+ try
+ {
+ if (JsonNode.Parse(File.ReadAllText(runtimeConfigPath)) is not { } root
+ || root["runtimeOptions"] is not JsonObject runtimeOptions)
+ {
+ return;
+ }
+
+ // Remove the dead doubly-nested block left by the old runtimeconfig.template.json
+ runtimeOptions.Remove("runtimeOptions");
+
+ if (runtimeOptions["configProperties"] is not JsonObject configProperties
+ || !configProperties.ContainsKey("EntryPointFilePath"))
+ {
+ return;
+ }
+
+ configProperties["EntryPointFilePath"] = $".\\{scriptName}";
+ configProperties["EntryPointFileDirectoryPath"] = ".";
+
+ File.WriteAllText(runtimeConfigPath, root.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Failed to rewrite runtime config paths for {scriptName}: {ex.Message}");
+ }
+}
+
+static async Task CleanScript(string scriptName, string scriptsDir, string outDir, string runtime,
CancellationToken cancellationToken)
{
Console.WriteLine($"Cleaning script: {scriptName} for runtime: {runtime}");
diff --git a/build-tools/build-scripts/runtimeconfig.template.json b/build-tools/build-scripts/runtimeconfig.template.json
deleted file mode 100644
index 835174faf..000000000
--- a/build-tools/build-scripts/runtimeconfig.template.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "runtimeOptions": {
- "configProperties": {
- "EntryPointFilePath": ".\\GBTest.cs",
- "EntryPointFileDirectoryPath": "."
- }
- }
-}
\ No newline at end of file
diff --git a/build-tools/linux-x64/GBTest b/build-tools/linux-x64/GBTest
index 8f68d49a4..b49deac07 100755
Binary files a/build-tools/linux-x64/GBTest and b/build-tools/linux-x64/GBTest differ
diff --git a/build-tools/linux-x64/GBTest.dll b/build-tools/linux-x64/GBTest.dll
index ea8b15112..749c86d89 100644
Binary files a/build-tools/linux-x64/GBTest.dll and b/build-tools/linux-x64/GBTest.dll differ
diff --git a/build-tools/linux-x64/GBTest.runtimeconfig.json b/build-tools/linux-x64/GBTest.runtimeconfig.json
index 6c47522f2..7ff48568f 100644
--- a/build-tools/linux-x64/GBTest.runtimeconfig.json
+++ b/build-tools/linux-x64/GBTest.runtimeconfig.json
@@ -12,8 +12,8 @@
}
},
"configProperties": {
- "EntryPointFilePath": "/Users/timpurdum/repos/GeoBlazor/GeoBlazor.Pro/GeoBlazor/build-tools/build-scripts/GBTest.cs",
- "EntryPointFileDirectoryPath": "/Users/timpurdum/repos/GeoBlazor/GeoBlazor.Pro/GeoBlazor/build-tools/build-scripts",
+ "EntryPointFilePath": "D:\\dymaptic.GeoBlazor.CodeGen\\GeoBlazor.Pro\\GeoBlazor\\build-tools\\build-scripts\\GBTest.cs",
+ "EntryPointFileDirectoryPath": "D:\\dymaptic.GeoBlazor.CodeGen\\GeoBlazor.Pro\\GeoBlazor\\build-tools\\build-scripts",
"Microsoft.Extensions.DependencyInjection.VerifyOpenGenericServiceTrimmability": true,
"System.ComponentModel.DefaultValueAttribute.IsSupported": false,
"System.ComponentModel.Design.IDesignerHost.IsSupported": false,
diff --git a/build-tools/linux-x64/Utilities.dll b/build-tools/linux-x64/Utilities.dll
index 302f41cf7..faf2d8902 100644
Binary files a/build-tools/linux-x64/Utilities.dll and b/build-tools/linux-x64/Utilities.dll differ
diff --git a/build-tools/osx-arm64/GBTest b/build-tools/osx-arm64/GBTest
index 67b0b835c..e3670f8fb 100755
Binary files a/build-tools/osx-arm64/GBTest and b/build-tools/osx-arm64/GBTest differ
diff --git a/build-tools/osx-arm64/GBTest.dll b/build-tools/osx-arm64/GBTest.dll
index df13fc9f4..4aee4ef77 100644
Binary files a/build-tools/osx-arm64/GBTest.dll and b/build-tools/osx-arm64/GBTest.dll differ
diff --git a/build-tools/osx-arm64/GBTest.runtimeconfig.json b/build-tools/osx-arm64/GBTest.runtimeconfig.json
index c0db3c7b0..1cf2450bf 100644
--- a/build-tools/osx-arm64/GBTest.runtimeconfig.json
+++ b/build-tools/osx-arm64/GBTest.runtimeconfig.json
@@ -12,8 +12,8 @@
}
},
"configProperties": {
- "EntryPointFilePath": "/Users/timpurdum/repos/GeoBlazor/GeoBlazor.Pro/GeoBlazor/build-tools/build-scripts/GBTest.cs",
- "EntryPointFileDirectoryPath": "/Users/timpurdum/repos/GeoBlazor/GeoBlazor.Pro/GeoBlazor/build-tools/build-scripts",
+ "EntryPointFilePath": "D:\\dymaptic.GeoBlazor.CodeGen\\GeoBlazor.Pro\\GeoBlazor\\build-tools\\build-scripts\\GBTest.cs",
+ "EntryPointFileDirectoryPath": "D:\\dymaptic.GeoBlazor.CodeGen\\GeoBlazor.Pro\\GeoBlazor\\build-tools\\build-scripts",
"Microsoft.Extensions.DependencyInjection.VerifyOpenGenericServiceTrimmability": true,
"System.ComponentModel.DefaultValueAttribute.IsSupported": false,
"System.ComponentModel.Design.IDesignerHost.IsSupported": false,
diff --git a/build-tools/osx-arm64/Utilities.dll b/build-tools/osx-arm64/Utilities.dll
index 302f41cf7..faf2d8902 100644
Binary files a/build-tools/osx-arm64/Utilities.dll and b/build-tools/osx-arm64/Utilities.dll differ
diff --git a/build-tools/win-x64/GBTest.dll b/build-tools/win-x64/GBTest.dll
index 2879d97fb..bca7aac54 100644
Binary files a/build-tools/win-x64/GBTest.dll and b/build-tools/win-x64/GBTest.dll differ
diff --git a/build-tools/win-x64/GBTest.exe b/build-tools/win-x64/GBTest.exe
index ec9ba2c5d..157e2f253 100755
Binary files a/build-tools/win-x64/GBTest.exe and b/build-tools/win-x64/GBTest.exe differ
diff --git a/build-tools/win-x64/GBTest.runtimeconfig.json b/build-tools/win-x64/GBTest.runtimeconfig.json
index ff061f894..0feb20dac 100644
--- a/build-tools/win-x64/GBTest.runtimeconfig.json
+++ b/build-tools/win-x64/GBTest.runtimeconfig.json
@@ -12,8 +12,8 @@
}
},
"configProperties": {
- "EntryPointFilePath": "/Users/timpurdum/repos/GeoBlazor/GeoBlazor.Pro/GeoBlazor/build-tools/build-scripts/GBTest.cs",
- "EntryPointFileDirectoryPath": "/Users/timpurdum/repos/GeoBlazor/GeoBlazor.Pro/GeoBlazor/build-tools/build-scripts",
+ "EntryPointFilePath": "D:\\dymaptic.GeoBlazor.CodeGen\\GeoBlazor.Pro\\GeoBlazor\\build-tools\\build-scripts\\GBTest.cs",
+ "EntryPointFileDirectoryPath": "D:\\dymaptic.GeoBlazor.CodeGen\\GeoBlazor.Pro\\GeoBlazor\\build-tools\\build-scripts",
"Microsoft.Extensions.DependencyInjection.VerifyOpenGenericServiceTrimmability": true,
"System.ComponentModel.DefaultValueAttribute.IsSupported": false,
"System.ComponentModel.Design.IDesignerHost.IsSupported": false,
diff --git a/build-tools/win-x64/Utilities.dll b/build-tools/win-x64/Utilities.dll
index 302f41cf7..faf2d8902 100644
Binary files a/build-tools/win-x64/Utilities.dll and b/build-tools/win-x64/Utilities.dll differ
diff --git a/src/dymaptic.GeoBlazor.Core/Components/ActiveLayerInfo.gb.cs b/src/dymaptic.GeoBlazor.Core/Components/ActiveLayerInfo.gb.cs
index b6e2ab9f5..ee4b6559d 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/ActiveLayerInfo.gb.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/ActiveLayerInfo.gb.cs
@@ -286,50 +286,6 @@ public ActiveLayerInfo(
return IsScaleDriven;
}
- ///
- /// Asynchronously retrieve the current value of the Layer property.
- ///
- public async Task GetLayer()
- {
- if (CoreJsModule is null)
- {
- return Layer;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return Layer;
- }
-
- Layer? result = await JsComponentReference.InvokeAsync(
- "getLayer", CancellationTokenSource.Token);
-
- if (result is not null)
- {
- if (Layer is not null)
- {
- result.Id = Layer.Id;
- }
- result.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
-
-#pragma warning disable BL0005
- Layer = result;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = Layer;
- }
-
- return Layer;
- }
///
/// Asynchronously retrieve the current value of the LayerView property.
@@ -733,48 +689,6 @@ await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
JsComponentReference, "hideLayersNotInCurrentView", value);
}
- ///
- /// Asynchronously set the value of the Layer property after render.
- ///
- ///
- /// The value to set.
- ///
- public async Task SetLayer(Layer? value)
- {
- if (value is not null)
- {
- value.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
- }
-
-#pragma warning disable BL0005
- Layer = value;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = value;
-
- if (CoreJsModule is null)
- {
- return;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return;
- }
-
- await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
- JsComponentReference, "layer", value);
- }
-
///
/// Asynchronously set the value of the LayerView property after render.
///
diff --git a/src/dymaptic.GeoBlazor.Core/Components/FeatureSnappingLayerSource.gb.cs b/src/dymaptic.GeoBlazor.Core/Components/FeatureSnappingLayerSource.gb.cs
index f5f8bc33c..e7ca0fff0 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/FeatureSnappingLayerSource.gb.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/FeatureSnappingLayerSource.gb.cs
@@ -99,50 +99,6 @@ public FeatureSnappingLayerSource(
return Enabled;
}
- ///
- /// Asynchronously retrieve the current value of the Layer property.
- ///
- public async Task GetLayer()
- {
- if (CoreJsModule is null)
- {
- return Layer;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return Layer;
- }
-
- Layer? result = await JsComponentReference.InvokeAsync(
- "getLayer", CancellationTokenSource.Token);
-
- if (result is not null)
- {
- if (Layer is not null)
- {
- result.Id = Layer.Id;
- }
- result.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
-
-#pragma warning disable BL0005
- Layer = result;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = Layer;
- }
-
- return Layer;
- }
#endregion
diff --git a/src/dymaptic.GeoBlazor.Core/Components/Graphic.cs b/src/dymaptic.GeoBlazor.Core/Components/Graphic.cs
index 8c5e21c29..90e482011 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/Graphic.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/Graphic.cs
@@ -196,7 +196,7 @@ public Graphic(
/// Retrieves the from the rendered graphic.
///
[CodeGenerationIgnore]
- public Task GetLayer()
+ public override Task GetLayer()
{
return Task.FromResult(Parent as Layer);
}
diff --git a/src/dymaptic.GeoBlazor.Core/Components/LayerSearchSource.gb.cs b/src/dymaptic.GeoBlazor.Core/Components/LayerSearchSource.gb.cs
index 4770ff324..178abd4a4 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/LayerSearchSource.gb.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/LayerSearchSource.gb.cs
@@ -335,50 +335,6 @@ public LayerSearchSource(
return ExactMatch;
}
- ///
- /// Asynchronously retrieve the current value of the Layer property.
- ///
- public async Task GetLayer()
- {
- if (CoreJsModule is null)
- {
- return Layer;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return Layer;
- }
-
- Layer? result = await JsComponentReference.InvokeAsync(
- "getLayer", CancellationTokenSource.Token);
-
- if (result is not null)
- {
- if (Layer is not null)
- {
- result.Id = Layer.Id;
- }
- result.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
-
-#pragma warning disable BL0005
- Layer = result;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = Layer;
- }
-
- return Layer;
- }
///
/// Asynchronously retrieve the current value of the Name property.
@@ -653,48 +609,6 @@ await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
JsComponentReference, "exactMatch", value);
}
- ///
- /// Asynchronously set the value of the Layer property after render.
- ///
- ///
- /// The value to set.
- ///
- public async Task SetLayer(Layer? value)
- {
- if (value is not null)
- {
- value.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
- }
-
-#pragma warning disable BL0005
- Layer = value;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = value;
-
- if (CoreJsModule is null)
- {
- return;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return;
- }
-
- await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
- JsComponentReference, "layer", value);
- }
-
///
/// Asynchronously set the value of the Name property after render.
///
diff --git a/src/dymaptic.GeoBlazor.Core/Components/LegendLayerInfos.gb.cs b/src/dymaptic.GeoBlazor.Core/Components/LegendLayerInfos.gb.cs
index fcce3af86..d2368b2d5 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/LegendLayerInfos.gb.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/LegendLayerInfos.gb.cs
@@ -37,50 +37,6 @@ public partial class LegendLayerInfos : MapComponent
#region Property Getters
- ///
- /// Asynchronously retrieve the current value of the Layer property.
- ///
- public async Task GetLayer()
- {
- if (CoreJsModule is null)
- {
- return Layer;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return Layer;
- }
-
- Layer? result = await JsComponentReference.InvokeAsync(
- "getLayer", CancellationTokenSource.Token);
-
- if (result is not null)
- {
- if (Layer is not null)
- {
- result.Id = Layer.Id;
- }
- result.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
-
-#pragma warning disable BL0005
- Layer = result;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = Layer;
- }
-
- return Layer;
- }
///
/// Asynchronously retrieve the current value of the SublayerIds property.
@@ -164,48 +120,6 @@ public partial class LegendLayerInfos : MapComponent
#region Property Setters
- ///
- /// Asynchronously set the value of the Layer property after render.
- ///
- ///
- /// The value to set.
- ///
- public async Task SetLayer(Layer? value)
- {
- if (value is not null)
- {
- value.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
- }
-
-#pragma warning disable BL0005
- Layer = value;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = value;
-
- if (CoreJsModule is null)
- {
- return;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return;
- }
-
- await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
- JsComponentReference, "layer", value);
- }
-
///
/// Asynchronously set the value of the SublayerIds property after render.
///
diff --git a/src/dymaptic.GeoBlazor.Core/Components/LegendViewModelLayerInfo.gb.cs b/src/dymaptic.GeoBlazor.Core/Components/LegendViewModelLayerInfo.gb.cs
index fd4ec2b20..4aed9fabf 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/LegendViewModelLayerInfo.gb.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/LegendViewModelLayerInfo.gb.cs
@@ -66,44 +66,6 @@ public LegendViewModelLayerInfo(
#region Property Getters
- ///
- /// Asynchronously retrieve the current value of the Layer property.
- ///
- public async Task GetLayer()
- {
- if (CoreJsModule is null)
- {
- return Layer;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return Layer;
- }
-
- // get the property value
- Layer? result = await JsComponentReference!.InvokeAsync("getProperty",
- CancellationTokenSource.Token, "layer");
- if (result is not null)
- {
-#pragma warning disable BL0005
- Layer = result;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = Layer;
- }
-
- return Layer;
- }
///
/// Asynchronously retrieve the current value of the SublayerIds property.
@@ -187,43 +149,6 @@ public LegendViewModelLayerInfo(
#region Property Setters
- ///
- /// Asynchronously set the value of the Layer property after render.
- ///
- ///
- /// The value to set.
- ///
- public async Task SetLayer(Layer? value)
- {
-#pragma warning disable BL0005
- Layer = value;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = value;
-
- if (CoreJsModule is null)
- {
- return;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return;
- }
-
- await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
- JsComponentReference, "layer", value);
- }
-
///
/// Asynchronously set the value of the SublayerIds property after render.
///
diff --git a/src/dymaptic.GeoBlazor.Core/Components/LegendViewModelLayerInfos.gb.cs b/src/dymaptic.GeoBlazor.Core/Components/LegendViewModelLayerInfos.gb.cs
index 7e7e23bf0..7cbebc635 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/LegendViewModelLayerInfos.gb.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/LegendViewModelLayerInfos.gb.cs
@@ -58,50 +58,6 @@ public LegendViewModelLayerInfos(
#region Property Getters
- ///
- /// Asynchronously retrieve the current value of the Layer property.
- ///
- public async Task GetLayer()
- {
- if (CoreJsModule is null)
- {
- return Layer;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return Layer;
- }
-
- Layer? result = await JsComponentReference.InvokeAsync(
- "getLayer", CancellationTokenSource.Token);
-
- if (result is not null)
- {
- if (Layer is not null)
- {
- result.Id = Layer.Id;
- }
- result.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
-
-#pragma warning disable BL0005
- Layer = result;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = Layer;
- }
-
- return Layer;
- }
///
/// Asynchronously retrieve the current value of the Title property.
@@ -146,48 +102,6 @@ public LegendViewModelLayerInfos(
#region Property Setters
- ///
- /// Asynchronously set the value of the Layer property after render.
- ///
- ///
- /// The value to set.
- ///
- public async Task SetLayer(Layer? value)
- {
- if (value is not null)
- {
- value.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
- }
-
-#pragma warning disable BL0005
- Layer = value;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = value;
-
- if (CoreJsModule is null)
- {
- return;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return;
- }
-
- await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
- JsComponentReference, "layer", value);
- }
-
///
/// Asynchronously set the value of the Title property after render.
///
diff --git a/src/dymaptic.GeoBlazor.Core/Components/ListItem.gb.cs b/src/dymaptic.GeoBlazor.Core/Components/ListItem.gb.cs
index 12dee0527..00d597273 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/ListItem.gb.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/ListItem.gb.cs
@@ -502,50 +502,6 @@ public ListItem()
return Incompatible;
}
- ///
- /// Asynchronously retrieve the current value of the Layer property.
- ///
- public async Task GetLayer()
- {
- if (CoreJsModule is null)
- {
- return Layer;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return Layer;
- }
-
- Layer? result = await JsComponentReference.InvokeAsync(
- "getLayer", CancellationTokenSource.Token);
-
- if (result is not null)
- {
- if (Layer is not null)
- {
- result.Id = Layer.Id;
- }
- result.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
-
-#pragma warning disable BL0005
- Layer = result;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = Layer;
- }
-
- return Layer;
- }
///
/// Asynchronously retrieve the current value of the LayerView property.
@@ -1058,48 +1014,6 @@ await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
JsComponentReference, "hidden", value);
}
- ///
- /// Asynchronously set the value of the Layer property after render.
- ///
- ///
- /// The value to set.
- ///
- public async Task SetLayer(Layer? value)
- {
- if (value is not null)
- {
- value.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
- }
-
-#pragma warning disable BL0005
- Layer = value;
-#pragma warning restore BL0005
- ModifiedParameters[nameof(Layer)] = value;
-
- if (CoreJsModule is null)
- {
- return;
- }
-
- try
- {
- JsComponentReference ??= await CoreJsModule.InvokeAsync(
- "getJsComponent", CancellationTokenSource.Token, Id);
- }
- catch (JSException)
- {
- // this is expected if the component is not yet built
- }
-
- if (JsComponentReference is null)
- {
- return;
- }
-
- await JsComponentReference.InvokeVoidAsync("setLayer",
- CancellationTokenSource.Token, value);
- }
-
///
/// Asynchronously set the value of the ListModeDisabled property after render.
///
diff --git a/src/dymaptic.GeoBlazor.Core/Components/MapComponent.razor.cs b/src/dymaptic.GeoBlazor.Core/Components/MapComponent.razor.cs
index 81c9c5ee0..95f68aad5 100644
--- a/src/dymaptic.GeoBlazor.Core/Components/MapComponent.razor.cs
+++ b/src/dymaptic.GeoBlazor.Core/Components/MapComponent.razor.cs
@@ -494,6 +494,95 @@ await CoreJsModule.InvokeVoidAsync("setProperty", CancellationTokenSource.Token,
return currentValue;
}
}
+
+ ///
+ /// Asynchronously retrieve the current value of the Layer property.
+ ///
+ [CodeGenerationIgnore]
+ public virtual async Task GetLayer()
+ {
+ if (CoreJsModule is null)
+ {
+ return Layer;
+ }
+
+ try
+ {
+ JsComponentReference ??= await CoreJsModule.InvokeAsync(
+ "getJsComponent", CancellationTokenSource.Token, Id);
+ }
+ catch (JSException)
+ {
+ // this is expected if the component is not yet built
+ }
+
+ if (JsComponentReference is null)
+ {
+ return Layer;
+ }
+
+ Layer? result =
+ await JsComponentReference.InvokeAsync("getLayer", CancellationTokenSource.Token);
+
+ if (result is not null)
+ {
+ if (Layer is not null)
+ {
+ result.Id = Layer.Id;
+ }
+
+ result.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
+
+#pragma warning disable BL0005
+ Layer = result;
+#pragma warning restore BL0005
+ ModifiedParameters[nameof(Layer)] = Layer;
+ }
+
+ return Layer;
+ }
+
+ ///
+ /// Asynchronously set the value of the Layer property after render.
+ ///
+ ///
+ /// The value to set.
+ ///
+ public virtual async Task SetLayer(Layer? value)
+ {
+ if (value is not null)
+ {
+ value.UpdateGeoBlazorReferences(CoreJsModule!, ProJsModule, View, this, Layer);
+ }
+
+#pragma warning disable BL0005
+ Layer = value;
+#pragma warning restore BL0005
+ ModifiedParameters[nameof(Layer)] = value;
+
+ if (CoreJsModule is null)
+ {
+ return;
+ }
+
+ try
+ {
+ JsComponentReference ??= await CoreJsModule.InvokeAsync(
+ "getJsComponent", CancellationTokenSource.Token, Id);
+ }
+ catch (JSException)
+ {
+ // this is expected if the component is not yet built
+ }
+
+ if (JsComponentReference is null)
+ {
+ return;
+ }
+
+ await JsComponentReference.InvokeVoidAsync("setLayer",
+ CancellationTokenSource.Token, value);
+ }
///
/// Called from to "Register" the current component with its parent.
@@ -1023,9 +1112,20 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
AbortManager ??= new AbortManager(CoreJsModule);
}
+ // A Layer can be resolved after this component's JS object was built (e.g. a layer added to
+ // the map after render and bound by id). buildJs* could not bind it then, so push the
+ // resolved layer to the JS component once it exists.
+ if (!_layerSyncedToJs && Layer is {} graphicsLayer && JsComponentReference is not null)
+ {
+ _layerSyncedToJs = true;
+ await SetLayer(graphicsLayer);
+ }
+
IsRenderedBlazorComponent = true;
}
+ private bool _layerSyncedToJs;
+
///
/// Tells the to completely re-render.
///
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/arcGisJsInterop.ts b/src/dymaptic.GeoBlazor.Core/Scripts/arcGisJsInterop.ts
index 942cb3d45..1320f3904 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/arcGisJsInterop.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/arcGisJsInterop.ts
@@ -125,7 +125,7 @@ export let queryLayer: FeatureLayer;
export let blazorServer: boolean = false;
export let ProtoGraphicCollection;
export let ProtoViewHitCollection;
-export let geometryEngine: GeometryEngineWrapper = new GeometryEngineWrapper(false);
+export let geometryEngine: GeometryEngineWrapper = new GeometryEngineWrapper(true);
export let projectionEngine: ProjectionWrapper = new ProjectionWrapper(false);
// region module variables
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/baseComponent.ts b/src/dymaptic.GeoBlazor.Core/Scripts/baseComponent.ts
index abf1eae12..ffd0dc5cc 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/baseComponent.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/baseComponent.ts
@@ -30,6 +30,27 @@ export default class BaseComponent implements IPropertyWrapper {
getProperty(prop: string) {
return this.component[prop];
}
+
+ async setLayer(value: any): Promise {
+ if ('layer' in this.component) {
+ let { buildJsLayer } = await import('./layer');
+ this.component.layer = await buildJsLayer(value, this.layerId, this.viewId);
+ }
+ }
+
+ async getLayer(): Promise {
+ if (hasValue(this.component.loadStatus) && this.component.loadStatus === 'not-loaded') {
+ await this.component.load();
+ }
+
+ if (!hasValue(this.component.layer)) {
+ return null;
+ }
+
+ let { buildDotNetLayer } = await import('./layer');
+ return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
+ }
+
unwrap() {
return this.component;
}
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/cSVLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/cSVLayerView.gb.ts
index 961c9874f..351acf06f 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/cSVLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/cSVLayerView.gb.ts
@@ -171,15 +171,6 @@ export default class CSVLayerViewGenerated extends BaseComponent {
this.component.highlightOptions = await buildJsHighlightOptions(value);
}
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
}
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/featureLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/featureLayerView.gb.ts
index 53f5e8055..825e5298f 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/featureLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/featureLayerView.gb.ts
@@ -109,15 +109,6 @@ export default class FeatureLayerViewGenerated extends BaseComponent {
this.component.highlightOptions = await buildJsHighlightOptions(value);
}
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetFeatureLayer } = await import('./featureLayer');
- return await buildDotNetFeatureLayer(this.component.layer, this.layerId, this.viewId);
- }
-
}
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/geoJSONLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/geoJSONLayerView.gb.ts
index 6726aca77..7052564da 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/geoJSONLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/geoJSONLayerView.gb.ts
@@ -171,15 +171,6 @@ export default class GeoJSONLayerViewGenerated extends BaseComponent {
this.component.highlightOptions = await buildJsHighlightOptions(value);
}
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
}
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/geoRSSLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/geoRSSLayerView.gb.ts
index 5daceac95..767f1b009 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/geoRSSLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/geoRSSLayerView.gb.ts
@@ -40,17 +40,6 @@ export default class GeoRSSLayerViewGenerated extends BaseComponent {
return await this.component.when(callback,
errback);
}
-
- // region properties
-
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
}
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/graphicsLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/graphicsLayerView.gb.ts
index e613b22d4..317687b0c 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/graphicsLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/graphicsLayerView.gb.ts
@@ -73,15 +73,6 @@ export default class GraphicsLayerViewGenerated extends BaseComponent {
this.component.highlightOptions = await buildJsHighlightOptions(value);
}
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
}
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/imageryLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/imageryLayerView.gb.ts
index 6903c2db8..dc77063a2 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/imageryLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/imageryLayerView.gb.ts
@@ -77,15 +77,6 @@ export default class ImageryLayerViewGenerated extends BaseComponent {
this.component.highlightOptions = await buildJsHighlightOptions(value);
}
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
async getPixelData(): Promise {
if (!hasValue(this.component.pixelData)) {
return null;
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/imageryTileLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/imageryTileLayerView.gb.ts
index 3d16accdf..3b11ee233 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/imageryTileLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/imageryTileLayerView.gb.ts
@@ -45,17 +45,6 @@ export default class ImageryTileLayerViewGenerated extends BaseComponent {
errback);
}
- // region properties
-
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
}
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/kMLLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/kMLLayerView.gb.ts
index 238aec05d..452fb155d 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/kMLLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/kMLLayerView.gb.ts
@@ -79,15 +79,6 @@ export default class KMLLayerViewGenerated extends BaseComponent {
return this.component.allVisiblePolylines!.map(i => buildDotNetGraphic(i, this.layerId, this.viewId));
}
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
}
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/listItem.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/listItem.gb.ts
index 6f348cd0f..0ca79e82a 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/listItem.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/listItem.gb.ts
@@ -87,20 +87,6 @@ export default class ListItemGenerated extends BaseComponent {
this.component.children = await Promise.all(value.map(async i => await buildJsListItem(i, this.layerId, this.viewId))) as any;
}
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
- async setLayer(value: any): Promise {
- let { buildJsLayer } = await import('./layer');
- this.component.layer = await buildJsLayer(value, this.layerId, this.viewId);
- }
-
async getLayerView(): Promise {
if (!hasValue(this.component.layerView)) {
return null;
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/sublayer.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/sublayer.gb.ts
index ee528e74a..784c703f0 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/sublayer.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/sublayer.gb.ts
@@ -286,19 +286,6 @@ export default class SublayerGenerated extends BaseComponent {
this.component.labelingInfo = await Promise.all(value.map(async i => await buildJsLabel(i, this.layerId, this.viewId))) as any;
}
- async getLayer(): Promise {
- if (this.component.loadStatus === 'not-loaded') {
- await this.component.load();
- }
-
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
async getObjectIdField(): Promise {
if (this.component.loadStatus === 'not-loaded') {
await this.component.load();
diff --git a/src/dymaptic.GeoBlazor.Core/Scripts/wFSLayerView.gb.ts b/src/dymaptic.GeoBlazor.Core/Scripts/wFSLayerView.gb.ts
index facd281f4..92bce80e2 100644
--- a/src/dymaptic.GeoBlazor.Core/Scripts/wFSLayerView.gb.ts
+++ b/src/dymaptic.GeoBlazor.Core/Scripts/wFSLayerView.gb.ts
@@ -169,17 +169,7 @@ export default class WFSLayerViewGenerated extends BaseComponent {
async setHighlightOptions(value: any): Promise {
let { buildJsHighlightOptions } = await import('./highlightOptions');
this.component.highlightOptions = await buildJsHighlightOptions(value);
- }
-
- async getLayer(): Promise {
- if (!hasValue(this.component.layer)) {
- return null;
- }
-
- let { buildDotNetLayer } = await import('./layer');
- return await buildDotNetLayer(this.component.layer, this.layerId, this.viewId);
- }
-
+ }
}
diff --git a/src/dymaptic.GeoBlazor.Core/Serialization/GeometryConverter.cs b/src/dymaptic.GeoBlazor.Core/Serialization/GeometryConverter.cs
index 6af3370b4..e7fc4929e 100644
--- a/src/dymaptic.GeoBlazor.Core/Serialization/GeometryConverter.cs
+++ b/src/dymaptic.GeoBlazor.Core/Serialization/GeometryConverter.cs
@@ -17,52 +17,69 @@ internal class GeometryConverter : JsonConverter
return null;
}
+ Geometry? geometry = null;
+
if (temp.TryGetValue("type", out object? typeValue))
{
switch (typeValue?.ToString())
{
case "extent":
- return JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ geometry = JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ break;
case "point":
- return JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ geometry = JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ break;
case "polygon":
- return JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ geometry = JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ break;
case "polyline":
- return JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ geometry = JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ break;
case "multipoint":
// multipoint is in GeoBlazor Pro and must be loaded via Reflection
Type? multipointType = Type.GetType("dymaptic.GeoBlazor.Pro.Components.Geometries.Multipoint, " +
"dymaptic.GeoBlazor.Pro");
if (multipointType is not null)
{
- return (Geometry?)JsonSerializer.Deserialize(ref cloneReader, multipointType, newOptions);
+ geometry = (Geometry?)JsonSerializer.Deserialize(ref cloneReader, multipointType, newOptions);
}
- return null;
+ break;
}
}
- if (temp.ContainsKey("rings"))
- {
- return JsonSerializer.Deserialize(ref cloneReader, newOptions);
- }
-
- if (temp.ContainsKey("paths"))
+ if (geometry is null)
{
- return JsonSerializer.Deserialize(ref cloneReader, newOptions);
- }
-
- if (temp.ContainsKey("latitude") || temp.ContainsKey("x"))
- {
- return JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ if (temp.ContainsKey("rings"))
+ {
+ geometry = JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ }
+ else if (temp.ContainsKey("paths"))
+ {
+ geometry = JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ }
+ else if (temp.ContainsKey("latitude") || temp.ContainsKey("x"))
+ {
+ geometry = JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ }
+ else if (temp.ContainsKey("xmax"))
+ {
+ geometry = JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ }
}
- if (temp.ContainsKey("xmax"))
+ // Operator results (e.g. Union) send a computed `extent`, but deserializing the concrete
+ // geometry type does not carry the nested Extent through; populate it explicitly so callers
+ // (e.g. map.GoTo(extent)) get a usable Extent.
+ if (geometry is not null and not Extent
+ && geometry.Extent is null
+ && temp.TryGetValue("extent", out object? extentValue)
+ && extentValue is JsonElement { ValueKind: JsonValueKind.Object } extentElement)
{
- return JsonSerializer.Deserialize(ref cloneReader, newOptions);
+ geometry.Extent = extentElement.Deserialize(newOptions);
}
- return null;
+ return geometry;
}
public override void Write(Utf8JsonWriter writer, Geometry value, JsonSerializerOptions options)
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Automation/GeoBlazorTestClass.cs b/test/dymaptic.GeoBlazor.Core.Test.Automation/GeoBlazorTestClass.cs
index 4196521a1..b83d8e32d 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Automation/GeoBlazorTestClass.cs
+++ b/test/dymaptic.GeoBlazor.Core.Test.Automation/GeoBlazorTestClass.cs
@@ -207,9 +207,23 @@ await _retryPipeline.ExecuteAsync(async ctx =>
Trace.WriteLine(messages, ProcessName.WEB_TEST);
Trace.WriteLine(errors, ProcessName.WEB_TEST_ERROR);
- TestConfig.FailedTests[ProcessName.WEB_TEST][testName] = $"{messages}{Environment.NewLine}{errors}";
-
- Assert.Fail($"{testName} Failed: {errors}");
+ // Capture the actual exception (Playwright timeout, assertion, navigation error, etc.) - not
+ // just browser console text, which is empty when the page fails before logging anything. Without
+ // this the FINAL_SUMMARY failure details were blank and the real cause was invisible.
+ string failureDetail = $"{ex.GetType().Name}: {ex.Message}";
+ if (!string.IsNullOrWhiteSpace(messages))
+ {
+ failureDetail += $"{Environment.NewLine}Console: {messages}";
+ }
+ if (!string.IsNullOrWhiteSpace(errors))
+ {
+ failureDetail += $"{Environment.NewLine}Errors: {errors}";
+ }
+ failureDetail += $"{Environment.NewLine}{ex.StackTrace}";
+
+ TestConfig.FailedTests[ProcessName.WEB_TEST][testName] = failureDetail;
+
+ Assert.Fail($"{testName} Failed: {ex.Message}");
}
finally
{
@@ -220,8 +234,20 @@ await _retryPipeline.ExecuteAsync(async ctx =>
private string BuildTestUrl(string testName)
{
+ // Map the enum to its name via a switch of compile-time nameof constants rather than interpolating
+ // the enum directly. Interpolation calls Enum.ToString -> EnumInfo.Create, which on every test was
+ // throwing BadImageFormatException/CLDB_E_INDEX_NOTFOUND (corrupt enum name metadata in this process).
+ // A switch on enum constants compiles to integer comparisons and needs no reflection metadata.
+ string renderMode = TestConfig.RenderMode switch
+ {
+ BlazorMode.Server => nameof(BlazorMode.Server),
+ BlazorMode.WebAssembly => nameof(BlazorMode.WebAssembly),
+ BlazorMode.Hybrid => nameof(BlazorMode.Hybrid),
+ _ => ((int)TestConfig.RenderMode).ToString()
+ };
+
return $"{TestConfig.TestAppUrl}?testFilter={testName}&renderMode={
- TestConfig.RenderMode}{(TestConfig.ProOnly ? "&proOnly" : "")}{(TestConfig.CoreOnly ? "&coreOnly" : "")}";
+ renderMode}{(TestConfig.ProOnly ? "&proOnly" : "")}{(TestConfig.CoreOnly ? "&coreOnly" : "")}";
}
private async Task Setup()
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Automation/TestConfig.cs b/test/dymaptic.GeoBlazor.Core.Test.Automation/TestConfig.cs
index 85fd401ad..482b85c5c 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Automation/TestConfig.cs
+++ b/test/dymaptic.GeoBlazor.Core.Test.Automation/TestConfig.cs
@@ -526,15 +526,37 @@ public static async Task AssemblyCleanup()
Trace.WriteLine("------------", ProcessName.FINAL_SUMMARY);
}
- // trim off extra timestamp from web browser and split lines
- string[] errorLines = failedTest.Value.Substring(26).Split(Environment.NewLine);
-
Trace.WriteLine($" {testCategory} - {failedTest.Key}",
ProcessName.FINAL_SUMMARY);
- foreach (string errorLine in errorLines)
+ try
+ {
+ // Trim off the optional "[timestamp] " prefix the browser logging prepends, but
+ // only when present. A hard Substring(26) previously threw on short messages
+ // (e.g. "Test Failed"), and because the enclosing block has no catch it aborted
+ // the entire FAILED TEST DETAILS section, leaving it blank.
+ string rawError = failedTest.Value ?? string.Empty;
+ string trimmedError = rawError;
+
+ if (rawError.StartsWith('['))
+ {
+ int closeIndex = rawError.IndexOf("] ", StringComparison.Ordinal);
+
+ if (closeIndex >= 0)
+ {
+ trimmedError = rawError[(closeIndex + 2)..];
+ }
+ }
+
+ foreach (string errorLine in trimmedError.Split(Environment.NewLine))
+ {
+ Trace.WriteLine($" {errorLine}", ProcessName.FINAL_SUMMARY);
+ }
+ }
+ catch (Exception ex)
{
- Trace.WriteLine($" {errorLine}", ProcessName.FINAL_SUMMARY);
+ Trace.WriteLine($" (could not format failure detail: {ex.Message})",
+ ProcessName.FINAL_SUMMARY);
}
}
}
@@ -857,6 +879,62 @@ private static async ValueTask LaunchWebTests(ResilienceContext context)
}
}
+ // Test projects are launched with --no-build (dotnet run / dotnet test), so they must be compiled
+ // first. On a clean runner nothing pre-builds them, and the web app and unit-test tasks run in
+ // parallel, so building concurrently races on the shared ESBuild lock (AcquireBuildLock exits with
+ // code 1). Serialize builds through a semaphore and build each project at most once per run.
+ private static readonly SemaphoreSlim _buildSemaphore = new(1, 1);
+ private static readonly HashSet _builtProjects = [];
+
+ private static async Task EnsureTestProjectBuilt(string projectPath, CancellationToken token)
+ {
+ await _buildSemaphore.WaitAsync(token);
+
+ try
+ {
+ if (!_builtProjects.Add(projectPath))
+ {
+ // Already built this run (e.g. a Polly retry of the same task).
+ return;
+ }
+
+ string projectName = Path.GetFileNameWithoutExtension(projectPath);
+ Trace.WriteLine($"Building {projectName} ({_runConfig})...", ProcessName.TEST_SETUP);
+
+ CommandResult buildResult = await Cli.Wrap("dotnet")
+ .WithArguments([
+ "build", projectPath,
+ "-c", _runConfig!,
+ "/p:GenerateXmlComments=false",
+ "/p:GeneratePackage=false",
+ "/p:GenerateDocs=false",
+ "/p:DebugSymbols=true",
+ "/p:DebugType=portable",
+ "/p:UsePackageReferences=false",
+ "/p:ShowScriptDialogs=false"
+ ])
+ .WithStandardOutputPipe(PipeTarget.ToDelegate(line => Trace.WriteLine(line, ProcessName.TEST_SETUP)))
+ .WithStandardErrorPipe(PipeTarget.ToDelegate(line => Trace.WriteLine(line, ProcessName.TEST_SETUP)))
+ .WithWorkingDirectory(ProjectFolder)
+ .WithValidation(CommandResultValidation.None)
+ .ExecuteAsync(token, gracefulCts.Token);
+
+ if (buildResult.ExitCode != 0)
+ {
+ // Allow a later retry to attempt the build again.
+ _builtProjects.Remove(projectPath);
+
+ throw new InvalidOperationException(
+ $"Failed to build {projectName} (exit code {buildResult.ExitCode}). "
+ + "See the TEST_SETUP output above for the build error.");
+ }
+ }
+ finally
+ {
+ _buildSemaphore.Release();
+ }
+ }
+
private static async ValueTask LaunchUnitTests(ResilienceContext context)
{
Stopwatch unitTestStopwatch = Stopwatch.StartNew();
@@ -970,6 +1048,11 @@ private static async ValueTask RunUnitTests(ResilienceContext context)
_ => throw new ArgumentOutOfRangeException(nameof(processName), processName, null)
};
+ // dotnet test runs below with --no-build, so the unit-test project must be compiled first. On a
+ // clean runner nothing else builds it, which previously caused "An error occurred trying to start
+ // process .../bin/Release/.../*.Test.Unit ... No such file or directory".
+ await EnsureTestProjectBuilt(testPath, context.CancellationToken);
+
string cmdLineApp = "dotnet";
List args =
@@ -1290,11 +1373,21 @@ private static async Task StartWebApp(CancellationToken token)
EnsureGeoBlazorLicenseKeyInUserSecrets(TestAppPath, licenseKey);
}
+ // The web app reads the ArcGIS API key from configuration key "ArcGISApiKey"; without it the basemap
+ // never loads and the render tests fail. CI provides it as the ARCGIS_API_KEY env var, but nothing
+ // injected it into the web app (only the license was), so the app fell back to the placeholder in
+ // appsettings.Development.json. Pass it through as the "ArcGISApiKey" env var, which ASP.NET maps onto
+ // that config key and which overrides appsettings regardless of environment.
+ string? apiKey = Configuration["ARCGIS_API_KEY"];
+
string cmdLineApp = "dotnet";
string[] args =
[
- "run", "--no-build", "--project", TestAppPath,
+ // -c must precede "--" so it sets the run/build configuration: with --no-build, dotnet run
+ // looks for the app under bin//, and without an explicit -c it defaults to Debug and
+ // fails to find the Release build produced by EnsureTestProjectBuilt ("No such file").
+ "run", "--no-build", "-c", _runConfig!, "--project", TestAppPath,
"--urls", $"{TestAppUrl};{TestAppHttpUrl}",
"--", "-c", _runConfig!,
"/p:GenerateXmlComments=false",
@@ -1327,6 +1420,13 @@ private static async Task StartWebApp(CancellationToken token)
];
}
+ // The web app is launched below with --no-build, so it must be compiled first. Nothing else in
+ // the test pipeline builds the test web app - the Automation project does not reference it - so on
+ // a clean runner (e.g. a hosted CI agent with no prior build output) the --no-build launch would
+ // have no assembly to run. Build it once, up front. (See EnsureTestProjectBuilt for why builds are
+ // serialized.)
+ await EnsureTestProjectBuilt(TestAppPath, token);
+
Trace.WriteLine($"Starting test app: {cmdLineApp} {string.Join(" ", args)}", ProcessName.WEB_APP_SERVER);
bool ioExceptionThrown = false;
@@ -1349,9 +1449,16 @@ private static async Task StartWebApp(CancellationToken token)
.WithStandardErrorPipe(PipeTarget.ToDelegate(line =>
Trace.WriteLine(line, ProcessName.WEB_APP_ERROR)))
.WithWorkingDirectory(ProjectFolder)
+ .WithEnvironmentVariables(apiKey is null
+ ? new Dictionary()
+ : new Dictionary { ["ArcGISApiKey"] = apiKey })
.ExecuteAsync(token, gracefulCts.Token);
processIds.Add(commandTask.ProcessId);
+ // Track the web-app process so WaitForHttpResponse can fail fast if it exits before becoming
+ // reachable, instead of polling for the full timeout. (Previously this was only set in the
+ // container path, so a crash in the non-container path went undetected until the 15m timeout.)
+ _webTestProcessId = commandTask.ProcessId;
try
{
@@ -1651,19 +1758,26 @@ private static async Task WaitForHttpResponse()
if (testProcess is not null && testProcess.HasExited)
{
+ int? exitCode = null;
+
try
{
- int exitCode = testProcess.ExitCode;
-
- if (exitCode != 0)
- {
- throw new ProcessExitedException($"Test process exited with code {exitCode}");
- }
+ exitCode = testProcess.ExitCode;
}
catch
{
// ignore - the container building process can exit silently and all is fine
}
+
+ // Read ExitCode defensively above, but throw outside the catch so a genuine non-zero
+ // exit actually propagates. (Previously the throw was inside the try and the bare catch
+ // swallowed it, so a crashed web app was never surfaced and the poll ran to timeout.)
+ if (exitCode is not null and not 0)
+ {
+ throw new ProcessExitedException(
+ $"Test web app process exited with code {exitCode} before becoming reachable. "
+ + "See the WEB_APP_SERVER / WEB_APP_ERROR output above for the cause.");
+ }
}
await Task.Delay(1000, Cts.Token);
diff --git a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WebMapTests.razor b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WebMapTests.razor
index e073d32c7..1fea007d7 100644
--- a/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WebMapTests.razor
+++ b/test/dymaptic.GeoBlazor.Core.Test.Blazor.Shared/Components/WebMapTests.razor
@@ -49,6 +49,7 @@
}
}
+ [CICondition(ConditionMode.Exclude)]
[TestMethod]
public async Task TestDymapticEnterpriseWebMaps(Action renderHandler)
{