Skip to content

409 Conflict when setting property values in properties pane #251

Description

@AndreKraemer

Issue Description

When using MAUI.Sherpa's Inspector to modify element properties, setting any property value on a selected element results in an an unhandled error has occured error

Steps to Reproduce

  1. Create a new .NET MAUI app using the default template (with "Hello, World!" label and counter button)

  2. Add the DevFlow Agent to the app:

    #if DEBUG
      builder.Logging.AddDebug();
      builder.AddMauiDevFlowAgent();
    #endif
  3. run the app on Windows

  4. Open MAUI.Sherpa and connect the app inspector to the running app

  5. In the visual tree, click on any element (e.g., the "Hello, World!" Label)

  6. In the Properties pane on the right, change any property value (e.g., FontSize, Text, etc.)

  7. Result: Unhandled Blazor exception appears

Environment

MAUI.Sherpa Version: v0.15.0
DevFlow Agent Version: 0.1.0-preview.12.26421.1
.NET MAUI Version: .NET MAUI 11 Preview 7
Platform: Windows (Sherpa and inspected app both running on Windows)
Other platforms: Not tested yet; Android/iOS testing planned.

Error Details:

Response status code does not indicate success: 409 (Conflict).
   at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
   at MauiSherpa.Core.Services.DevFlowV1Client.SetPropertyAsync(String elementId, String propertyName, Object value, CancellationToken ct)
   at MauiSherpa.AppInspector.Services.InspectorUiClient.SetPropertyAsync(String elementId, String propertyName, String value, CancellationToken ct)
   at MauiSherpa.AppInspector.Pages.Inspector.DevFlowTreeTab.<>c__DisplayClass99_0.<<RenderEnumEditor>b__1>d.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

Suspected Root Cause

The DevFlow agent validates capture epochs/generations when performing UI mutations. The current Sherpa implementation does not appear to pass the capture metadata obtained from GetTreeAsync() to SetPropertyAsync().

The suspected flow is:

  • Sherpa calls GetTreeAsync() to capture the visual tree and receives capture metadata including an epoch/generation.
  • Sherpa does not appear to cache/pass this metadata when subsequently calling SetPropertyAsync().
  • SetPropertyAsync() in DevFlowV1Client currently sends only the property value, with no capture epoch/generation:
    var response = await _http.PutAsJsonAsync(url, new { value }, JsonOptions, ct);
  • The DevFlow agent's HandleSetProperty() calls PrepareUiMutationAsync(), which validates the capture state.
  • Based on the DevFlow agent code, this validation may reject the mutation when the capture epoch is considered stale, resulting in a 409 Conflict (potentially with a stale-capture-epoch reason).
  • The HTTP error is not handled by Sherpa and ultimately surfaces as an unhandled Blazor exception.

Notably, the 409 occurs even when the app has not been interacted with or visibly changed after connecting Sherpa. Refreshing/reloading the Inspector tree immediately before changing the property also does not prevent the 409. This suggests that an actual UI mutation between tree capture and property mutation is unlikely to be the sole cause.

Expected Behavior

Setting property values should succeed without an HTTP 409 error.

If the mutation cannot be applied because the Inspector capture is stale or otherwise invalid, Sherpa should handle the conflict gracefully instead of surfacing an unhandled Blazor exception.

Additional Context

  • This reproduces consistently for me.

  • The 409 occurs even when the app has not been interacted with or visibly changed after connecting Sherpa.

  • Refreshing/reloading the Inspector tree immediately before changing the property does not prevent the 409.

  • Related code:

  • SetPropertyAsync in DevFlowV1Client:

    public async Task<object?> SetPropertyAsync(string elementId, string propertyName, object value, CancellationToken ct = default)
    {
    var url = $"/api/v1/ui/elements/{Uri.EscapeDataString(elementId)}/properties/{Uri.EscapeDataString(propertyName)}";
    var response = await _http.PutAsJsonAsync(url, new { value }, JsonOptions, ct);
    response.EnsureSuccessStatusCode();
    var result = await response.Content.ReadFromJsonAsync<JsonElement>(JsonOptions, ct);
    return result.TryGetProperty("value", out var val) ? val.Deserialize<object>(JsonOptions) : null;
    }

  • RenderEnumEditor in DevFlowTreeTab.razor:

    __builder.AddAttribute(8, "onchange", EventCallback.Factory.Create<ChangeEventArgs>(this, async (e) =>
    {
    if (selectedElement != null)
    {
    await Client.SetPropertyAsync(selectedElement.Id, prop.Name, e.Value?.ToString() ?? "");
    await RefreshTree();
    await RefreshProperties();
    }
    }));
    if (prop.Meta.EnumValues != null)
    {
    foreach (var val in prop.Meta.EnumValues)
    {
    __builder.OpenElement(10, "option");
    __builder.AddAttribute(11, "value", val);
    if (val.Equals(prop.Value, StringComparison.OrdinalIgnoreCase))
    __builder.AddAttribute(12, "selected", true);
    __builder.AddContent(13, val);
    __builder.CloseElement();
    }
    }
    __builder.CloseElement(); // select
    __builder.CloseElement(); // div
    };
    private RenderFragment RenderColorEditor(PropertyEntry prop) => __builder =>
    {
    var cssColor = MauiColorToCss(prop.Value);

  • CommitEdit in DevFlowTreeTab.razor:

    var prop = editingProperty;
    var val = editValue;
    var elementId = selectedElement.Id;
    // Cache CSS value for brush properties (MAUI ToString doesn't preserve gradient stops)
    var meta = GetMeta(prop);
    if (meta.Editor == EditorType.Brush && !string.IsNullOrEmpty(val))
    {
    brushValueCache[$"{elementId}:{prop}"] = val;
    }
    editingProperty = null;
    gradientStops.Clear();
    selectedStopIndex = null;
    isDraggingStop = false;
    StateHasChanged();
    await Client.SetPropertyAsync(elementId, prop, val);
    await RefreshTree();
    await RefreshProperties();

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions