Skip to content

Add the same menu items for subscriptions as for queues - #880

Merged
ErikMogensen merged 5 commits into
paolosalvatori:mainfrom
ErikMogensen:subscription-menu
Jul 17, 2026
Merged

ErikMogensen merged 5 commits into
paolosalvatori:mainfrom
ErikMogensen:subscription-menu

Conversation

@ErikMogensen

Copy link
Copy Markdown
Collaborator

There are several menu items that are missing from the message grid for dead-letter queue messages in subscriptions. This PR brings makes them available for subscriptions too.

Removed the word 'shared' from some menu items variable names since they weren't shared.

This causes some code duplication, but that will be handled in a separate PR.

Copilot AI review requested due to automatic review settings April 28, 2026 20:26
@ErikMogensen

Copy link
Copy Markdown
Collaborator Author

@claude, do a review of this handsome pr

@claude

claude Bot commented Apr 28, 2026 •

Copy link
Copy Markdown

Claude finished @ErikMogensen's task in 2m 7s —— View job


PR Review: Add the same menu items for subscriptions as for queues

  • Gather context and understand the request
  • Read changed files and analyze the diff
  • Review for bugs, correctness, and consistency
  • Review for code quality and best practices
  • Post final review feedback

Overall this is a clean, well-scoped PR that achieves its goal. The renames are sensible, the new code follows the established patterns in HandleQueueControl, and the author is transparent about the intentional code duplication. A few items worth noting:


Observations

1. Missing "Select All" in the subscription deadletter context menu

HandleQueueControl includes a selectAllSharedDeadletterMessagesToolStripMenuItem (Ctrl+A) in its deadletter context menu. This PR adds selectAllMessagesToolStripMenuItem to the subscription's messages menu, but the subscription's deadletter context menu (deadletterContextMenuStrip) doesn't get a corresponding "Select All". This looks like an oversight.

HandleSubscriptionControl.Designer.cs line ~1695 — the deadletter context menu items don't include a select-all entry, even though the queue's equivalent does (HandleQueueControl.Designer.cs line ~2180). Fix this →


2. Indentation inconsistency at end of HandleSubscriptionControl.Designer.cs

The last two field declarations and the closing braces have incorrect indentation relative to the rest of the file:

// Current (incorrectly indented)
        private System.Windows.Forms.PropertyGrid deadletterCustomPropertyGrid;
                 private System.Windows.Forms.ToolStripMenuItem resubmitDeadletterToolStripMenuItem;
                 private System.Windows.Forms.ToolStripMenuItem resubmitMessageToolStripMenuItem;

            }
        }

resubmitDeadletterToolStripMenuItem and resubmitMessageToolStripMenuItem appear to have been moved from an old position with extra leading whitespace, and the closing braces are at the wrong depth. Doesn't affect compilation, but is inconsistent. Fix this →


3. selectAllMessagesToolStripMenuItem order is slightly inconsistent with queue

In the subscription messages menu, the new item is ordered as:

Repair And Resubmit → Resubmit → **Select All** → Resubmit In Batch → separator → Save...

But in HandleQueueControl's deadletter menu, the equivalent order is:

Repair And Resubmit → Resubmit → Resubmit In Batch → **Select All** → separator → Save...

Minor UX inconsistency between the two controls' menus.


4. Pre-existing filename splitting bug (carried forward, not introduced here)

Both the queue and subscription multi-save handlers use this pattern:

// HandleSubscriptionControl.cs ~line 2953
var fileNameParts = saveFileDialog.FileName.Split('.').ToList();
var fileExtension = fileNameParts.Last();
fileNameParts.RemoveAt(fileNameParts.IndexOf(fileExtension));  // BUG: finds first match, not last

IndexOf finds the first occurrence, but Last() returns the last element. For a file like my.txt.file.txt, this would remove the wrong segment. This isn't introduced here (it's already in HandleQueueControl), but since the code was copied, it's worth flagging for the eventual refactor PR.


What's Good

  • The rename of sharedDeadletterContextMenuStrip → deadletterContextMenuStrip and removal of Shared from field names is a clean, correct cleanup. Event handler method names are intentionally preserved.
  • New handler implementations in HandleSubscriptionControl.cs follow the same pattern as HandleQueueControl.cs faithfully.
  • CreateFileNameAutoRecognize() helper correctly mirrors the queue control.
  • The PR description is honest about the duplication and the follow-up plan.
  • selectAllMessagesToolStripMenuItem correctly wires up Ctrl+A.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns the subscription (including subscription DLQ) message grid context menus with the queue message grid by adding missing actions (notably “save message text as file” and “select all”), and it also renames several queue deadletter menu item variables to drop the misleading “shared” wording.

Changes:

  • Add context-menu actions in HandleSubscriptionControl to save selected message body text to file (single + multi) and to select all messages.
  • Add corresponding ToolStrip menu items/wiring in the subscription control designer.
  • Rename queue deadletter context menu strip + menu item fields to remove “Shared” from variable names.

Reviewed changes

Copilot reviewed 2 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/ServiceBusExplorer/Controls/HandleSubscriptionControl.cs Adds “save body as file” (single/multi) for messages and DLQ messages; adds “select all messages”; updates context-menu visibility logic.
src/ServiceBusExplorer/Controls/HandleSubscriptionControl.Designer.cs Adds ToolStrip items and click wiring for the new subscription context-menu actions.
src/ServiceBusExplorer/Controls/HandleQueueControl.cs Updates deadletter menu visibility wiring to use renamed (non-“shared”) menu item fields and context menu strip.
src/ServiceBusExplorer/Controls/HandleQueueControl.Designer.cs Renames deadletter context menu strip + menu item fields; updates designer wiring accordingly.
Files not reviewed (2)
  • src/ServiceBusExplorer/Controls/HandleQueueControl.Designer.cs: Language not supported
  • src/ServiceBusExplorer/Controls/HandleSubscriptionControl.Designer.cs: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2950 to +2957
foreach (var body in bodies)
{
count++;
var fileNameParts = saveFileDialog.FileName.Split('.').ToList();
var fileExtension = fileNameParts.Last();
fileNameParts.RemoveAt(fileNameParts.IndexOf(fileExtension));
fileNameParts.Add($"({count}).{fileExtension}");
var fileName = string.Join(".", fileNameParts);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The numbered-output filename generation is fragile: splitting the full path on '.' and removing the extension via IndexOf can remove the wrong segment when the filename contains multiple dots (e.g. "foo.txt.v1.txt") or the extension token appears earlier. Use Path.GetFileNameWithoutExtension/Path.GetExtension (or RemoveAt(fileNameParts.Count-1)) to build "{base}({count}){ext}" safely, preserving the directory portion.

Suggested change
foreach (var body in bodies)
{
count++;
var fileNameParts = saveFileDialog.FileName.Split('.').ToList();
var fileExtension = fileNameParts.Last();
fileNameParts.RemoveAt(fileNameParts.IndexOf(fileExtension));
fileNameParts.Add($"({count}).{fileExtension}");
var fileName = string.Join(".", fileNameParts);
var selectedFileName = saveFileDialog.FileName;
var directoryName = Path.GetDirectoryName(selectedFileName);
var baseFileName = Path.GetFileNameWithoutExtension(selectedFileName);
var fileExtension = Path.GetExtension(selectedFileName);
foreach (var body in bodies)
{
count++;
var numberedFileName = $"{baseFileName}({count}){fileExtension}";
var fileName = string.IsNullOrEmpty(directoryName)
? numberedFileName
: Path.Combine(directoryName, numberedFileName);

Copilot uses AI. Check for mistakes.
Comment on lines +3132 to +3136
var fileNameParts = saveFileDialog.FileName.Split('.').ToList();
var fileExtension = fileNameParts.Last();
fileNameParts.RemoveAt(fileNameParts.IndexOf(fileExtension));
fileNameParts.Add($"({count}).{fileExtension}");
var fileName = string.Join(".", fileNameParts);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here as in the messages bulk-save: generating numbered filenames by splitting on '.' and removing via IndexOf can produce incorrect paths when the chosen filename includes multiple dots. Prefer Path-based filename manipulation (GetDirectoryName + GetFileNameWithoutExtension + GetExtension) or at least remove the last segment rather than the first match.

Suggested change
var fileNameParts = saveFileDialog.FileName.Split('.').ToList();
var fileExtension = fileNameParts.Last();
fileNameParts.RemoveAt(fileNameParts.IndexOf(fileExtension));
fileNameParts.Add($"({count}).{fileExtension}");
var fileName = string.Join(".", fileNameParts);
var directoryName = Path.GetDirectoryName(saveFileDialog.FileName);
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(saveFileDialog.FileName);
var fileExtension = Path.GetExtension(saveFileDialog.FileName);
var numberedFileName = $"{fileNameWithoutExtension}({count}){fileExtension}";
var fileName = string.IsNullOrEmpty(directoryName)
? numberedFileName
: Path.Combine(directoryName, numberedFileName);

Copilot uses AI. Check for mistakes.
Comment on lines +2226 to +2238
// saveSelectedDeadletteredMessageToolStripMenuItem
//
this.saveSelectedSharedDeadletteredMessageToolStripMenuItem.Name = "saveSelectedSharedDeadletteredMessageToolStripMenuItem";
this.saveSelectedSharedDeadletteredMessageToolStripMenuItem.Size = new System.Drawing.Size(305, 22);
this.saveSelectedSharedDeadletteredMessageToolStripMenuItem.Text = "Save Selected Message";
this.saveSelectedSharedDeadletteredMessageToolStripMenuItem.Click += new System.EventHandler(this.saveSelectedSharedDeadletteredMessageToolStripMenuItem_Click);
this.saveSelectedDeadletteredMessageToolStripMenuItem.Name = "saveSelectedDeadletteredMessageToolStripMenuItem";
this.saveSelectedDeadletteredMessageToolStripMenuItem.Size = new System.Drawing.Size(305, 22);
this.saveSelectedDeadletteredMessageToolStripMenuItem.Text = "Save Selected Message";
this.saveSelectedDeadletteredMessageToolStripMenuItem.Click += new System.EventHandler(this.saveSelectedSharedDeadletteredMessageToolStripMenuItem_Click);
//
// saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem
// saveSelectedDeadletteredMessageBodyAsFileToolStripMenuItem
//
this.saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem.Name = "saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem";
this.saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem.Size = new System.Drawing.Size(305, 22);
this.saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem.Text = "Save Selected Message Text as File";
this.saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem.Click += new System.EventHandler(this.saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem_Click);
this.saveSelectedDeadletteredMessageBodyAsFileToolStripMenuItem.Name = "saveSelectedDeadletteredMessageBodyAsFileToolStripMenuItem";
this.saveSelectedDeadletteredMessageBodyAsFileToolStripMenuItem.Size = new System.Drawing.Size(305, 22);
this.saveSelectedDeadletteredMessageBodyAsFileToolStripMenuItem.Text = "Save Selected Message Text as File";
this.saveSelectedDeadletteredMessageBodyAsFileToolStripMenuItem.Click += new System.EventHandler(this.saveSelectedSharedDeadletteredMessageBodyAsFileToolStripMenuItem_Click);

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These menu items were renamed to remove the "Shared" wording, but their Click handlers still point to methods named SharedDeadletter (e.g. saveSelectedSharedDeadletteredMessageToolStripMenuItem_Click). This mismatch makes it harder to understand/refactor the UI wiring; consider renaming the handler methods (and updating references) to match the new menu item names.

Copilot uses AI. Check for mistakes.
@ErikMogensen

Copy link
Copy Markdown
Collaborator Author

Looks like good feedback. I'll take a deeper look at it and update this PR later.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 4 changed files in this pull request and generated 5 comments.

Files not reviewed (2)
  • src/ServiceBusExplorer/Controls/HandleQueueControl.Designer.cs: Generated file
  • src/ServiceBusExplorer/Controls/HandleSubscriptionControl.Designer.cs: Generated file

Comment on lines +2958 to +2962
var fileNameParts = saveFileDialog.FileName.Split('.').ToList();
var fileExtension = fileNameParts.Last();
fileNameParts.RemoveAt(fileNameParts.IndexOf(fileExtension));
fileNameParts.Add($"({count}).{fileExtension}");
var fileName = string.Join(".", fileNameParts);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an old issue, but I'll fix it.

Comment thread src/ServiceBusExplorer/Controls/HandleSubscriptionControl.cs Outdated
Comment thread src/ServiceBusExplorer/Controls/HandleSubscriptionControl.Designer.cs Outdated
Comment thread src/ServiceBusExplorer/Controls/HandleQueueControl.Designer.cs
Comment thread src/ServiceBusExplorer/Controls/HandleQueueControl.cs
ErikMogensen and others added 2 commits July 14, 2026 13:04
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@ErikMogensen

Copy link
Copy Markdown
Collaborator Author

Finally this is now ready for review.

Repository owner deleted a comment from claude Bot Jul 17, 2026

@paolosalvatori paolosalvatori left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Overall assessment: Solid parity work. The subscription (and subscription DLQ) message grids now offer the same actions as the queue grids: save-body-as-file (single + multi), and Select All in both context menus. I checked the new handlers line-by-line against their HandleQueueControl counterparts — the right grid, binding source, and text box are used in each one (txtMessageText/messagesDataGridView for the messages menu, txtDeadletterText/deadletterDataGridView for the dead-letter menu). Menu item ordering now mirrors the queue's corresponding menus exactly, including Select All placement. The PathHelper.GetNumberedFileName extraction is the standout: it fixes the long-standing first-match-removal filename bug at all four call sites (both controls — the queue's dead-letter save paths route through SaveSelectedMessages, so they're covered too), and no Split('.') occurrences remain in either control. Utilities.csproj is SDK-style, so the new file is picked up, and both controls already import ServiceBusExplorer.Utilities.Helpers. Build and unit tests pass on CI.

Key risks:

  1. Merge-order coordination with #886. Both PRs edit the same lines of HandleQueueControl.Designer.cs from the same base, with different content (this PR keeps the old *Shared* handler names in the Click hookups and renames sharedDeadletterContextMenuStrip; #886 renames the handlers but keeps the strip name). I verified locally: merging this PR into main is clean, but merging #886 afterwards conflicts in HandleQueueControl.Designer.cs (either order conflicts). Recommendation: land this PR first, then rebase #886 and resolve — the resolution is mechanical (keep #886's handler renames + this PR's strip rename).
  2. Nothing else of substance — the change is UI-only, no Service Bus API behavior is touched.

Status of prior review conversations (verdicts per thread):

  1. Copilot 2026-04-28 · HandleSubscriptionControl.cs:2957 (fragile filename split) — never replied to, left unresolved, but fully addressed in code by 27cf790 ("Fixed old path issue" → PathHelper). Obsolete → safe to close.
  2. Copilot 2026-04-28 · HandleSubscriptionControl.cs:3136 (same issue, second site) — never replied to, left unresolved, addressed by 27cf790. Obsolete → safe to close.
  3. Copilot 2026-04-28 · HandleQueueControl.Designer.cs:2238 (menu items renamed but handlers still *Shared*) — never replied to, left unresolved. Deliberately deferred to #886 (per your reply on the newer duplicate thread), so: still valid until #886 lands; suggest replying with the #886 pointer and closing, like its duplicate.
  4. Copilot 2026-07-14 · HandleSubscriptionControl.cs:2962 — replied "I'll fix it", fixed by 27cf790, but left unresolved → safe to close.
  5. Copilot 2026-07-14 · HandleSubscriptionControl.cs:3141 — fixed by 27cf790, resolved ✓.
  6. Copilot 2026-07-14 · designer-file tail indentation — fixed (verified the file tail on the branch), resolved ✓.
  7. Copilot 2026-07-14 · HandleQueueControl.Designer.cs:2232 — resolved with "handled in #886"; accurate for the six save/delete handler renames ✓ (contingent on #886 actually landing).
  8. Copilot 2026-07-14 · HandleQueueControl.cs:4384 (rename ShowAppropriateSharedDeadletterMenuItems) — resolved with "handled in #886", but #886 does not rename that method (nor RepairAndResubmitSharedDeadletterMessage or the repair/resubmit/select-all *Shared* items). Needs author action: extend #886 or reopen/track this one.

From the claude[bot] issue-comment review of 2026-04-28 (never replied to, but all four points were addressed by later commits): missing Select All in the sub dead-letter menu → added in bd1ee3c ✓; designer indentation → fixed ✓; menu ordering → now mirrors the queue's messages menu ✓; filename bug → fixed via PathHelper ✓. Housekeeping: the 2026-07-17 @claude review once run errored without posting (actions run 29563797543).

Recommendation: Comment — effectively approve once (a) the four stale-but-fixed Copilot threads (items 1, 2, 3, 4 above) get a reply/close, and (b) the #886 merge order is agreed. Inline notes below are minor/nit.

{
public static class PathHelper
{
public static string GetNumberedFileName(string fileName, int count)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor (test coverage) — good extraction: Path-API based, fixes the first-match-removal bug Copilot flagged twice (report.txt.backup.txt now numbers correctly). This is the one pure, dependency-free unit in the PR and the repo already has ServiceBusExplorer.Tests with helper tests — please add a small test locking in exactly the cases the review threads were about:

[Test]
public void GetNumberedFileName_HandlesMultiDotNames() =>
    Assert.AreEqual(@"C:\tmp\report.txt.backup(2).txt",
        PathHelper.GetNumberedFileName(@"C:\tmp\report.txt.backup.txt", 2));

plus a name without an extension ("file" → "file(1)") and a bare relative name (Path.GetDirectoryName returns "" there — the ?? string.Empty only matters for root paths).

// deleteSelectedDeadletterMessageToolStripMenuItem
//
this.deleteSelectedMessageToolStripMenuItem.Name = "deleteSelectedSharedDeadletterMessageToolStripMenuItem";
this.deleteSelectedMessageToolStripMenuItem.Name = "deleteSelectedDeadletterMessageToolStripMenuItem";

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — the .Name string and the comment header now say deleteSelectedDeadletterMessageToolStripMenuItem, but the field is still deleteSelectedMessageToolStripMenuItem (same for the plural one at line 1783). The mismatch predates this PR, but since the point of the rename pass is aligning identifiers, consider renaming the two fields to match. The WinForms designer treats Name as the component's serialization identity, and keeping field ↔ Name aligned avoids confusion the next time the designer regenerates this file.

private System.Windows.Forms.ToolStripMenuItem repairAndResubmitMessageToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem resubmitSelectedMessagesInBatchModeToolStripMenuItem;
private System.Windows.Forms.ContextMenuStrip sharedDeadletterContextMenuStrip;
private System.Windows.Forms.ContextMenuStrip deadletterContextMenuStrip;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — the sharedDeadletterContextMenuStrip → deadletterContextMenuStrip rename didn't reach HandleQueueControl.resx, which still carries (line 238). Harmless at runtime (designer-only tray metadata), but the stale key will linger until the designer rewrites the file — worth renaming in the resx while you're here.

Also noting for other readers: the Click hookups on the renamed items above intentionally still point at the old *Shared* handler names — that's what #886 fixes; see the summary comment about merge order.

foreach (var body in bodies)
{
count++;
var fileName = PathHelper.GetNumberedFileName(saveFileDialog.FileName, count);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (inherited behavior, for the follow-up refactor) — the SaveFileDialog's overwrite prompt covers foo.txt, but the loop actually writes foo(1).txt, foo(2).txt, … and silently File.Deletes any existing files with those names. Identical to the queue control, so consistent here — flagging it so the planned dedup PR can decide whether numbered targets deserve their own overwrite check.

@paolosalvatori
paolosalvatori self-requested a review July 17, 2026 09:11

@paolosalvatori paolosalvatori left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Erik,

Thanks for this PR and for your patience with the long review cycle on it — the subscription grids finally behaving like the queue grids is something users have been missing for a while, and the PathHelper extraction quietly fixes that old filename bug everywhere, which I really appreciate.

I approve this PR, but please take a look at the comments I left this morning and make sure to implement them. To be explicit about what I'm asking:

  1. Add a few unit tests for PathHelper.GetNumberedFileName in ServiceBusExplorer.Tests. It's a pure function, so it's cheap to test, and it's exactly the thing the two Copilot threads complained about — I'd like the fix locked in. The cases I care about: a multi-dot name (report.txt.backup.txt → report.txt.backup(2).txt), a name without an extension, and a bare relative filename.

  2. Fix the leftover in HandleQueueControl.resx — line 238 still says sharedDeadletterContextMenuStrip.TrayLocation; please rename it to match the new deadletterContextMenuStrip name so the designer metadata stays consistent.

  3. Small one, your call: in HandleSubscriptionControl.Designer.cs the delete items' .Name strings now say deleteSelectedDeadletterMessage(s)ToolStripMenuItem but the fields are still deleteSelectedMessage(s)ToolStripMenuItem. Since you're normalizing names anyway, aligning the fields too would avoid designer surprises later.

  4. Please close the review threads that are already fixed — the two April threads and the July one about the filename split are all addressed by your 27cf790 commit but still show as open, and the April thread about the handler names just needs the same "#886" reply you gave its July duplicate. It'll make the PR page much easier to read.

One important coordination point: this PR needs to merge before #886. They touch the same lines in HandleQueueControl.Designer.cs and whichever lands second will conflict — I verified it locally. So let's land this one, then rebase #886 on top.

The overwrite-prompt quirk on the numbered files I flagged inline is inherited from the queue control — totally fine to leave for your planned dedup refactor.

Thanks again, great work on this one!

Paolo

@ErikMogensen

ErikMogensen commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks @paolosalvatori! I don't know how you set it up, but impressive review! I'll merge this one and add unit tests.

@ErikMogensen
ErikMogensen merged commit f6316bc into paolosalvatori:main Jul 17, 2026
10 of 11 checks passed
@ErikMogensen
ErikMogensen deleted the subscription-menu branch July 17, 2026 09:35
@paolosalvatori

Copy link
Copy Markdown
Owner

@ErikMogensen I used Claude Code to review PRs 😅

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants