Override toolbar button properties and click behavior
Jacob builds the runbook authoring tool at a managed-services provider. Engineers use his app to draft, version, and ship the troubleshooting runbooks their NOC team follows at three in the morning. Runbooks are HTML documents with screenshots, command snippets, and tables of escalation contacts. The editor is WinFormHtmlEditor.
The annoyance that's been on Jacob's desk for a quarter is the Save button. When an engineer clicks the toolbar's Save icon, the editor pops a standard SaveFileDialog - the OS file picker. That made sense in 2014. In 2026 Jacob's runbooks are not files on disk; they are versioned documents in his app's SQL backend, with audit trails, peer-review requirements, and a custom commit-message dialog. He needs the editor's Save button to call his pipeline, not show a file picker.
While he is at it, he also wants the disk icon on the Save button to match the rest of his app's visual language, and he wants the tooltip to say "Commit runbook (Ctrl+S)" instead of the generic "Save".
Layer 1: Jacob fixes the icon and tooltip in code
Every built-in toolbar item is exposed as a typed ToolStripButton property on WinFormHtmlEditor (BtnSave, BtnNew, BtnPrint, …), and the same live items are reachable via ToolbarItemOverrider.ToolbarItems. Set the icon and tooltip from your form's Load handler (or right after InitializeComponent()). Built-in items are intentionally not editable through the Visual Studio Properties window or designer - design-time edits do not persist, and serializing the editor's own toolbar into your form is explicitly unsupported. Code is the single reliable path.
private void RunbookForm_Load(object sender, EventArgs e)
{
htmlEditor1.BtnSave.Image = MyIcons.CommitRunbook16;
htmlEditor1.BtnSave.ToolTipText = "Commit runbook (Ctrl+S)";
}Private Sub RunbookForm_Load(sender As Object, e As EventArgs)
htmlEditor1.BtnSave.Image = MyIcons.CommitRunbook16
htmlEditor1.BtnSave.ToolTipText = "Commit runbook (Ctrl+S)"
End SubLayer 2: runtime tweaks for state-dependent UI
Some of Jacob's state has to live in code. The Save button should be disabled when the runbook hasn't changed, hidden entirely when the engineer is in read-only review mode, and re-enabled after every keystroke. He reaches the same button from code:
private void RunbookForm_Load(object sender, EventArgs e)
{
htmlEditor1.BtnSave.Visible = !isReviewMode;
htmlEditor1.BtnSave.Enabled = false;
htmlEditor1.HtmlChanged += (s, a) => htmlEditor1.BtnSave.Enabled = true;
// Hide Paste-From-Word for engineers without an Office license.
htmlEditor1.BtnPasteFromMsWord.Visible = userHasOfficeLicense;
}Private Sub RunbookForm_Load(sender As Object, e As EventArgs)
htmlEditor1.BtnSave.Visible = Not isReviewMode
htmlEditor1.BtnSave.Enabled = False
htmlEditor1.HtmlChanged += Function(s, a) CSharpImpl.__Assign(htmlEditor1.BtnSave.Enabled, True)
' Hide Paste-From-Word for engineers without an Office license.
htmlEditor1.BtnPasteFromMsWord.Visible = userHasOfficeLicense
End Sub
Private Class CSharpImpl
<System.Obsolete("Please refactor calling code to use normal Visual Basic assignment")>
Shared Function __Assign(Of T)(ByRef target As T, value As T) As T
target = value
Return value
End FunctionEvery built-in button is reachable the same way. The full set Jacob has on tap: BtnNew, BtnOpen, BtnSave, BtnCut, BtnCopy, BtnPaste, BtnPasteFromMsWord, BtnBold, BtnItalic, BtnUnderline, BtnFormatReset, BtnFormatUndo, BtnFormatRedo, BtnPrint, BtnSpellCheck, BtnSearch, BtnHighlightColor, BtnFontColor, BtnHyperlink, BtnImage, BtnInsertYouTubeVideo, BtnTable, BtnSymbol, BtnHorizontalRule, BtnOrderedList, BtnUnOrderedList, BtnAlignLeft, BtnAlignCenter, BtnAlignRight, BtnOutdent, BtnIndent, BtnStrikeThrough, BtnSuperScript, BtnSubscript, BtnBodyStyle. Combos: CmbFontName, CmbFontSize, CmbTitleInsert.
Layer 3: actually replacing the click behavior
The icon and tooltip changes only made the button look different. The click still opens the OS file picker - that is what Jacob really needs to replace. He subscribes to ToolbarItemOverrider.SaveButtonClicked. The rule, baked into every override handler in the helper class, is that when a customer handler is attached, the editor's default action does not run - your code does instead.
private void RunbookForm_Load(object sender, EventArgs e)
{
htmlEditor1.ToolbarItemOverrider.SaveButtonClicked += (s, e) =>
{
// Skip the OS file picker entirely. Run the company commit pipeline.
var commitMessage = CommitMessageDialog.ShowDialog(htmlEditor1);
if (commitMessage == null)
return;
var html = htmlEditor1.Content.GetDocumentHtml();
RunbookRepository.Commit(currentRunbookId, html, commitMessage, currentUser);
htmlEditor1.BtnSave.Enabled = false; // re-enabled on next HtmlChanged
};
}Private Sub RunbookForm_Load(sender As Object, e As EventArgs)
htmlEditor1.ToolbarItemOverrider.SaveButtonClicked += Sub(s, e)
' Skip the OS file picker entirely. Run the company commit pipeline.
Dim commitMessage = CommitMessageDialog.ShowDialog(htmlEditor1)
If commitMessage Is Nothing Then Return
Dim html = htmlEditor1.Content.GetDocumentHtml()
RunbookRepository.Commit(currentRunbookId, html, commitMessage, currentUser)
htmlEditor1.BtnSave.Enabled = False ' re-enabled on next HtmlChanged
End Sub
End Sub
The toolbar button still looks like a Save button (Jacob's icon, Jacob's tooltip). The keyboard shortcut still fires the same handler. But the action is his commit pipeline - audit trail, peer-review queue, SQL backend.
The full override menu
Jacob ends up overriding three buttons total: Save (commit pipeline), New (clear-with-confirmation), and Print (render-to-PDF service). The same pattern works for everything else. Events open for override on ToolbarItemOverrider include BoldButtonClicked, ItalicButtonClicked, UnderlineButtonClicked, ImageButtonClicked, HyperLinkButtonClicked, TableInsertButtonClicked, NewButtonClicked, OpenButtonClicked, SaveButtonClicked, PrintButtonClicked, SpellCheckButtonClicked, SearchButtonClicked, SymbolInsertButtonClicked, YouTubeVideoInsertButtonClicked, plus the combo-value-changed events FontNameComboValueChanged, FontSizeComboValueChanged, and TitleInsertComboValueChanged. The complete set lives in ToolbarItemOverrideHelper.
Picking the right layer
- Just change icon, tooltip, or visibility? Set the
BtnXxxproperty at runtime (formLoad/ afterInitializeComponent). Built-in items have no design-time / Properties-grid path, by design. - Add work alongside the editor's action - audit log, telemetry, validation? Subscribe to
ToolbarItemOverrider.XxxButtonClickedand call the matchingFormatting/Content/Editorservice yourself afterward. - Replace the action entirely with your own dialog or pipeline? Subscribe and do not call the original service.