-
Notifications
You must be signed in to change notification settings - Fork 0
Add SketchWidget as Custom Editor Pro sample #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+293
−0
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
samples/core/dymaptic.GeoBlazor.Core.Sample.Shared/wwwroot/images/sketchEditor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
236 changes: 236 additions & 0 deletions
236
samples/pro/dymaptic.GeoBlazor.Pro.Sample.Shared/Pages/SketchAsEditor.razor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| @page "/sketch-as-editor" | ||
| @inherits SamplePage | ||
|
|
||
| <PageTitle>Sketch Widget as Custom Editor</PageTitle> | ||
| <h1>Sketch Widget as Custom Editor</h1> | ||
|
|
||
| <p class="instructions"> | ||
| This sample drives a <code>SketchWidget</code> programmatically from C# while presenting a fully custom UI. | ||
| The widget's container is parked in a hidden <code><div></code> via <code>ContainerId</code>, so its | ||
| built-in toolbar stays out of view and the only controls you see are the custom <strong>Move</strong>, | ||
| <strong>Save</strong>, and <strong>Cancel</strong> buttons below. | ||
| </p> | ||
| <p class="instructions"> | ||
| Click the <strong>Move</strong> button to arm the editor, then click any polygon on the map to grab it. | ||
| Drag the highlighted feature to a new location, then click <strong>Save</strong> to commit the move — the new | ||
| geometry is sent through <code>FeatureLayer.ApplyEdits</code>, the staging graphic is cleared, and the layer | ||
| is refreshed so the feature redraws at its updated position. Click <strong>Cancel</strong> (or press | ||
| <strong>Esc</strong>) at any point to abandon the edit. If you click somewhere that isn't a feature, the | ||
| editor disarms and reports "No feature at that point" — click <strong>Move</strong> again to retry. | ||
| </p> | ||
|
|
||
| <div class="spaced-row editor-toolbar"> | ||
| @if (_editing) | ||
| { | ||
| <button type="button" class="btn btn-primary" @onclick="SaveMove">Save</button> | ||
| <button type="button" class="btn btn-secondary" @onclick="CancelMove">Cancel</button> | ||
| } | ||
| else | ||
| { | ||
| <button type="button" | ||
| class="btn @(_moveArmed ? "btn-accent" : "btn-primary")" | ||
| @onclick="ArmMove" | ||
| disabled="@_moveArmed"> | ||
| @(_moveArmed ? "Click a feature… (Esc to cancel)" : "Move") | ||
| </button> | ||
| } | ||
| @if (_statusMessage is not null) | ||
| { | ||
| <span class="status-pill" role="status">@_statusMessage</span> | ||
| } | ||
| </div> | ||
|
|
||
| @* Host element for the SketchWidget; hidden because this sample drives the widget programmatically. *@ | ||
| <div id="sketch-as-editor-host" style="display:none;"></div> | ||
|
|
||
| <MapView @ref="_mapView" | ||
| Class="map-view" | ||
| OnClick="OnMapClick" | ||
| OnKeyDown="OnKeyDown" | ||
| Zoom="4"> | ||
| <Map> | ||
| <Basemap> | ||
| <BasemapStyle Name="BasemapStyleName.ArcgisNavigation"/> | ||
| </Basemap> | ||
| <FeatureLayer @ref="_featureLayer" | ||
| Source="@(_featureSource)" | ||
| GeometryType="FeatureGeometryType.Polygon" | ||
| ObjectIdField="OBJECTID" | ||
| Title="Editable Polygons"/> | ||
| <GraphicsLayer @ref="_sketchLayer"/> | ||
| </Map> | ||
| <SketchWidget @ref="_sketchWidget" | ||
| GraphicsLayer="@_sketchLayer" | ||
| OnUpdate="HandleSketchEvent" | ||
| ContainerId="sketch-as-editor-host"/> | ||
| </MapView> | ||
|
|
||
| @code { | ||
|
|
||
| public override List<NavMenu.PageLink> PageLinks => | ||
| [ | ||
| new("https://developers.arcgis.com/javascript/latest/references/core/widgets/Sketch/", "ArcGIS Maps SDK for JavaScript") | ||
| ]; | ||
|
|
||
| public override string Description => | ||
| "This GeoBlazor Pro sample, written in Blazor for .NET developers, demonstrates how to repurpose the " + | ||
| "SketchWidget as a programmatic move editor while presenting a fully custom UI. The widget's container is " + | ||
| "parked in a hidden div via ContainerId, so its built-in toolbar stays out of the view and the only " + | ||
| "controls the user sees are the custom Move, Save, and Cancel buttons. A custom Move button arms an " + | ||
| "OnMapClick handler that hit-tests the target FeatureLayer, stages the picked Graphic into a companion " + | ||
| "GraphicsLayer, and hands it to SketchWidget.Update with a SketchToolUpdateOptions configured for " + | ||
| "SketchTool.Move. A Save button calls SketchWidget.Complete() to finish the active update; the resulting " + | ||
| "SketchUpdateEvent is forwarded to FeatureLayer.ApplyEdits to persist the new geometry in the client-side " + | ||
| "in-memory source layer, then the staging graphic is removed and the feature layer is refreshed so it redraws " + | ||
| "at the updated location. The same ApplyEdits call works equally well against a hosted FeatureService — simply " + | ||
| "replace the in-memory Source with a Url pointing to your service and the rest of the pattern is unchanged. " + | ||
| "A Cancel button (and the Esc key) route through OnKeyDown to SketchWidget.Cancel(), " + | ||
| "which fires a Complete event with Aborted=true so ApplyEdits is skipped. The pattern is useful any time " + | ||
| "you need SketchViewModel's interactive move semantics inside a custom workflow with your own editor UI."; | ||
|
|
||
| private void ArmMove() | ||
| { | ||
| _moveArmed = true; | ||
| _statusMessage = null; | ||
| } | ||
|
|
||
| private async Task OnMapClick(ClickEvent evt) | ||
| { | ||
| if (!_moveArmed || _mapView is null || _featureLayer is null || _sketchWidget is null || _sketchLayer is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // One-shot: disarm immediately so a missed click doesn't stay sticky. | ||
| _moveArmed = false; | ||
|
|
||
| HitTestOptions options = new() | ||
| { | ||
| IncludeByGeoBlazorId = [_featureLayer.Id] | ||
| }; | ||
| HitTestResult hit = await _mapView.HitTest(evt, options); | ||
| Graphic? graphic = hit.Results.OfType<GraphicHit>().FirstOrDefault()?.Graphic; | ||
|
|
||
| if (graphic is null) | ||
| { | ||
| _statusMessage = "No feature at that point."; | ||
| return; | ||
| } | ||
|
|
||
| // Stage the picked graphic into the Sketch's own GraphicsLayer so the widget can operate on it. | ||
| await _sketchLayer.Add(graphic); | ||
| await _sketchWidget.Update([graphic], _updateOptions); | ||
| _editing = true; | ||
| _statusMessage = null; | ||
| } | ||
|
|
||
| private async Task SaveMove() | ||
| { | ||
| if (_sketchWidget is null) return; | ||
| // Tell SketchViewModel to finish the active update; HandleSketchEvent will run ApplyEdits. | ||
| await _sketchWidget.Complete(); | ||
| } | ||
|
|
||
| private async Task CancelMove() | ||
| { | ||
| if (_sketchWidget is null) return; | ||
| await _sketchWidget.Cancel(); | ||
| } | ||
|
|
||
| private async Task OnKeyDown(KeyDownEvent evt) | ||
| { | ||
| if (evt.Key != "Escape" || _sketchWidget is null) return; | ||
|
|
||
| if (_editing) | ||
| { | ||
| await _sketchWidget.Cancel(); | ||
| } | ||
| else if (_moveArmed) | ||
| { | ||
| _moveArmed = false; | ||
| _statusMessage = "Cancelled."; | ||
| } | ||
| } | ||
|
|
||
| private async Task HandleSketchEvent(SketchUpdateEvent evt) | ||
| { | ||
| if (evt.State != SketchEventState.Complete) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| _editing = false; | ||
| Graphic? moved = evt.Graphics?.FirstOrDefault(); | ||
|
|
||
| // Cancel() fires Complete with Aborted=true; skip ApplyEdits but still clean up the staging graphic. | ||
| if (evt.Aborted == true) | ||
| { | ||
| if (moved is not null) | ||
| { | ||
| await _sketchLayer!.Remove(moved); | ||
| } | ||
|
Comment on lines
+162
to
+171
|
||
| _statusMessage = "Cancelled."; | ||
| StateHasChanged(); | ||
| return; | ||
| } | ||
|
|
||
| if (moved is null || _featureLayer is null) return; | ||
|
|
||
| try | ||
| { | ||
| FeatureEditsResult result = await _featureLayer.ApplyEdits(new FeatureEdits | ||
| { | ||
| UpdateFeatures = [moved] | ||
| }); | ||
|
|
||
| FeatureEditResult? firstError = result.UpdateFeatureResults | ||
| .FirstOrDefault(r => r.Error is not null); | ||
|
|
||
| if (firstError is not null) | ||
| { | ||
| _statusMessage = $"Edit rejected: {firstError.Error!.Message}"; | ||
| } | ||
| else | ||
| { | ||
| _statusMessage = moved.Attributes.ContainsKey("OBJECTID") | ||
| ? $"Moved OBJECTID {moved.Attributes["OBJECTID"]}." | ||
| : "Moved."; | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _statusMessage = $"ApplyEdits failed: {ex.Message}"; | ||
| } | ||
| finally | ||
| { | ||
| // Clean up the staging graphic and reload the feature layer so it shows the new geometry. | ||
| await _sketchLayer!.Remove(moved); | ||
| await _featureLayer.Refresh(); | ||
| StateHasChanged(); | ||
| } | ||
| } | ||
|
|
||
| // Configure the SketchWidget to perform a Move update; all other options use their defaults. | ||
| private readonly SketchToolUpdateOptions _updateOptions = | ||
| new(SketchTool.Move, null, null, null, | ||
| null, null, null, | ||
| null!, null!); | ||
|
|
||
| private MapView? _mapView; | ||
| private FeatureLayer? _featureLayer; | ||
| private GraphicsLayer? _sketchLayer; | ||
| private SketchWidget? _sketchWidget; | ||
|
|
||
| private bool _moveArmed; | ||
| private bool _editing; | ||
| private string? _statusMessage; | ||
|
|
||
| private readonly List<Graphic> _featureSource = | ||
| [ | ||
| new(new Polygon([ | ||
| new MapPath(new MapPoint(0, 0), | ||
| new MapPoint(10, 0), new MapPoint(10, 10), | ||
| new MapPoint(0, 10), new MapPoint(0, 0)) | ||
| ]), attributes: new AttributesDictionary(new Dictionary<string, object?> { ["OBJECTID"] = 1 })) | ||
| ]; | ||
| } | ||
36 changes: 36 additions & 0 deletions
36
samples/pro/dymaptic.GeoBlazor.Pro.Sample.Shared/Pages/SketchAsEditor.razor.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| .editor-toolbar { | ||
| justify-content: flex-start; | ||
| gap: 1rem; | ||
| padding: 0.75rem 1rem; | ||
| } | ||
|
|
||
| .editor-toolbar .btn { | ||
| min-width: 18rem; | ||
| transition: background-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; | ||
| } | ||
|
|
||
| .editor-toolbar .btn.btn-accent { | ||
| box-shadow: 0 0 0 0.2rem var(--geoblazor-accent-hover); | ||
| animation: editor-pulse 1.4s ease-in-out infinite; | ||
| } | ||
|
|
||
| @keyframes editor-pulse { | ||
| 0%, 100% { | ||
| box-shadow: 0 0 0 0.15rem var(--geoblazor-accent-hover); | ||
| } | ||
| 50% { | ||
| box-shadow: 0 0 0 0.35rem var(--geoblazor-accent-hover); | ||
| } | ||
| } | ||
|
|
||
| .status-pill { | ||
| display: inline-flex; | ||
| align-items: center; | ||
| padding: 0.4rem 0.9rem; | ||
| border-radius: var(--box-radius); | ||
| background-color: var(--background-grey-2); | ||
| color: var(--text); | ||
| border: 1px solid var(--background-grey-3); | ||
| font-size: 1rem; | ||
| line-height: 1.2; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 30fe1e4 — added
_sketchLayer is nullto the early-return guard inOnMapClickand removed the null-forgiving!operator from the subsequent_sketchLayer.Add(graphic)call.