d2phap/ImageGlass · warning · ArgumentException

A button with the ID '{0}' has already been defined. Please

Error message

A button with the ID '{0}' has already been defined. Please choose a different and unique ID for your button to avoid conflicts.

What it means

Thrown in FrmSettings (Tab Toolbar handler) during button creation (isCreate == true) when the submitted ToolbarItemModel.Id already matches an existing Config.ToolbarButtons entry, case-insensitively. It is an ArgumentException whose message is built with ZString.Format from the localized resource FrmSettings.Toolbar._Errors._ButtonIdDuplicated, with {0} replaced by the offending Id. The exception is caught by the surrounding try/catch and shown via Config.ShowError, and the validation result posted back is false.

Source

Thrown at v9/ImageGlass/FrmSettings.cs:251

            var isValid = true;

            try
            {
                // try parsing the json
                var btn = BHelper.ParseJson<ToolbarItemModel>(e.Data);

                if (btn.Type == ToolbarItemModelType.Button)
                {
                    var langPath = $"{nameof(FrmSettings)}.Toolbar._Errors";
                    if (string.IsNullOrWhiteSpace(btn.Id))
                    {
                        throw new ArgumentException(Config.Language[$"{langPath}._ButtonIdRequired"], nameof(btn.Id));
                    }

                    if (isCreate
                        && Config.ToolbarButtons.Any(i => i.Id.Equals(btn.Id, StringComparison.OrdinalIgnoreCase)))
                    {
                        throw new ArgumentException(ZString.Format(Config.Language[$"{langPath}._ButtonIdDuplicated"], btn.Id), nameof(btn.Id));
                    }

                    if (string.IsNullOrEmpty(btn.OnClick.Executable))
                    {
                        throw new ArgumentException(Config.Language[$"{langPath}._ButtonExecutableRequired"], nameof(btn.OnClick.Executable));
                    }
                }
            }
            catch (Exception ex)
            {
                _ = Config.ShowError(this, title: Config.Language["_._Error"], heading: ex.Message);
                isValid = false;
            }

            Web2.PostWeb2Message(e.Name, BHelper.ToJson(isValid));
        }
        #endregion // Tab Toolbar

View on GitHub (pinned to 4a3c4fecef)

Solutions

  1. Choose a new, case-insensitively unique Id for the button being created.
  2. On edit (isCreate == false) reuse the existing Id instead of treating it as a new create.
  3. Add client-side dedupe: fetch the current toolbar IDs and disable submit when the Id collides.
  4. Normalize Ids to lowercase slugs at authoring time so casing collisions cannot occur.

Example fix

// before: submitting a create with an existing Id
var btn = new ToolbarItemModel
{
    Type = ToolbarItemModelType.Button,
    Id = "open-notepad",  // already exists in Config.ToolbarButtons
    OnClick = new ToolbarClickAction { Executable = "notepad.exe" },
};

// after: derive a unique Id from the existing set
var taken = Config.ToolbarButtons.Select(b => b.Id.ToLowerInvariant()).ToHashSet();
var baseId = "open-notepad";
var id = baseId;
var n = 2;
while (taken.Contains(id.ToLowerInvariant())) { id = $"{baseId}-{n++}"; }

var btn = new ToolbarItemModel
{
    Type = ToolbarItemModelType.Button,
    Id = id,
    OnClick = new ToolbarClickAction { Executable = "notepad.exe" },
};
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check for a duplicate Id (create path) before submitting to the handler
static bool IsUniqueIdForCreate(ToolbarItemModel btn)
{
    if (btn?.Type != ToolbarItemModelType.Button || string.IsNullOrWhiteSpace(btn.Id))
        return true; // other validations handle these
    return !Config.ToolbarButtons.Any(i => i.Id.Equals(btn.Id, StringComparison.OrdinalIgnoreCase));
}

Type guard

static bool IsCreatableButton(ToolbarItemModel btn, IEnumerable<ToolbarItemModel> existing) =>
    btn != null
    && btn.Type == ToolbarItemModelType.Button
    && !string.IsNullOrWhiteSpace(btn.Id)
    && !existing.Any(i => i.Id.Equals(btn.Id, StringComparison.OrdinalIgnoreCase));

Try / catch

try
{
    // submit create
}
catch (ArgumentException ex) when (ex.ParamName == "btn.Id" && ex.Message.Contains("already been defined"))
{
    // Surface a clear 'pick a different Id' message; this is expected for duplicates.
    isValid = false;
}

Prevention

When it happens

Trigger: The Web2 'Btn_AddCustomToolbarButton_ValidateJson_Create' message submits a button whose Id equals (case-insensitive) the Id of any entry already in Config.ToolbarButtons; e.g. re-adding a button after a refresh without changing the Id.

Common situations: A user duplicates an existing button config and forgets to rename the Id; the Id uses different casing of an existing Id ('Open' vs 'open'); or a re-import of a previously saved toolbar set is treated as a create.

Related errors


AI-assisted analysis of d2phap/ImageGlass@4a3c4fecef (2026-08-13). Data as JSON: /api/errors/f91b4f468a25cf1a. Report an issue: GitHub.