Make your own buttons context-aware

Ana works on the desktop authoring client at an academic-journal publisher. Editors and copy-editors spend their days in her app, marking up manuscripts before they go to typesetting. The product has its own toolbar - a Word-style ribbon she built three years ago, with custom icons sized to match the rest of the app. Underneath, the engine is WinFormHtmlEditor, with the factory toolbars hidden (the same pattern Olu uses on the legal-tech ribbon).

The bug-tracker ticket on Ana's desk this morning came from the head copy-editor: "When I click into bold text, your Bold button doesn't look pressed. Word's does. It's disorienting - I can't tell what state I'm in without re-bolding to check." The complaint is fair. Ana wired her ribbon buttons to ToolbarItemOverrider.OnBoldButtonClicked two years ago, but she never wired anything to push the editor's current state back to her own controls. Bold could be on, italic could be on, the caret could be in a hyperlink - her toolbar never updated.

The two pieces Ana needs

Two members on WinFormHtmlEditor solve this. The first is the event: SelectionChanged. It fires every time the caret moves or the selection changes - typing, arrow keys, mouse clicks, paste, programmatic edits. The second is the service: StateQuery. It is an IStateQueryService, the same service the built-in toolbar uses to drive its own pressed-state logic. Ana hooks the first, queries the second, updates her controls.

private void ManuscriptForm_Load(object sender, EventArgs e)

{

    htmlEditor1.SelectionChanged += HtmlEditor_SelectionChanged;

}



private void HtmlEditor_SelectionChanged(object sender, EventArgs e)

{

    var sq = htmlEditor1.StateQuery;



    // Toggle pressed state on the ribbon buttons.

    ribbonBoldButton.Checked      = sq.IsBold();

    ribbonItalicButton.Checked    = sq.IsItalic();

    ribbonUnderlineButton.Checked = sq.IsUnderline();



    // Highlight Insert Image when an image is the active element.

    ribbonImageButton.BackColor   = sq.IsImage()

                                  ? SystemColors.Highlight

                                  : SystemColors.Control;



    // Show the active font in the status bar.

    lblActiveFont.Text            = sq.GetActiveFontFamilyName();

    lblActiveSize.Text            = sq.GetActiveFontSize();

}

Ana's ribbon buttons reflecting bold, italic, underline, and image-active state via SelectionChanged

What StateQuery exposes

Reading the IStateQueryService interface, Ana realises she has access to far more than she needs for this ticket - which is fine, because the same handler will grow over the next few sprints as the copy-editors ask for more.

  • Inline formatting: IsBold(), IsItalic(), IsUnderline(), IsStrikeThrough(), IsSuperscript(), IsSubscript().
  • Block formatting: IsOrderedList(), IsUnorderedList(), IsJustifyLeft(), IsJustifyCenter(), IsJustifyRight(), IsActiveElementHeaderTitle(), GetActiveHeaderTitleNumber().
  • Active element kind: IsHyperLink(), IsImage(), IsTable(), IsTableCell(), IsYouTubeVideo(), IsActiveOrAncestorElementTable(), IsActiveOrAncestorElementHyperLink().
  • Font: GetActiveFontFamilyName(), GetActiveFontSize(), GetActiveForeColor(), GetActiveHighlightColor().
  • Edit-state guards: CanUndo(), CanRedo(), CanCut(), CanCopy(), CanPaste(), CanDelete(), CanMergeTableCells().
  • Low-level access: GetActiveHtmlElement() returns the raw IHTMLElement when she needs to inspect attributes the high-level helpers don't cover.

The sprint-2 ticket: Undo/Redo enabled state

Two weeks later the copy-editor lead opens another ticket: the ribbon's Undo and Redo buttons stay enabled even when there is nothing to undo or redo, so clicking them does nothing and people think the app is frozen. Ana extends the same handler:

private void HtmlEditor_SelectionChanged(object sender, EventArgs e)

{

    var sq = htmlEditor1.StateQuery;



    ribbonBoldButton.Checked      = sq.IsBold();

    ribbonItalicButton.Checked    = sq.IsItalic();

    ribbonUnderlineButton.Checked = sq.IsUnderline();



    ribbonUndoButton.Enabled      = sq.CanUndo();

    ribbonRedoButton.Enabled      = sq.CanRedo();

    ribbonPasteButton.Enabled     = sq.CanPaste();

}

Ana's custom Undo/Redo buttons enabled or disabled from CanUndo/CanRedo via SelectionChanged

The performance footnote

By sprint 4 Ana has fifteen pieces of state being recomputed on every SelectionChanged. The handler fires often - the copy-editors type fast - so she keeps the body cheap. The calls into StateQuery are inexpensive, but she avoids anything heavy inside the handler: no network calls, no file I/O, no layout-triggering reflows of large panels. Anything that wants to react to selection changes but cannot run synchronously gets deferred to a short-debounce System.Windows.Forms.Timer so a long edit session doesn't turn into a stutter.

The head copy-editor closes the original ticket the day Ana ships sprint 1 with a one-line comment: "Feels like Word now. Thank you."

Last updated on May 15, 2026