Making Your Own Buttons Context-Aware via SelectionChanged and StateQuery

Tara is a senior dev at a Dublin legal-tech startup. Her team is building a contract drafting tool for in-house counsel, and the chosen visual language is a familiar one: a top-anchored ribbon, Office-style, with grouped tabs for Home, Insert, Review, and Layout. The drafting surface itself is a WpfHtmlEditor embedded under the ribbon.

Tara has wired her ribbon's Bold, Italic, Underline, Bullet List, and Align buttons through ToolbarItemOverrider already (page 156's pattern), so clicks work. The problem now is the reverse direction. When the lawyer puts the caret inside bold text, Tara's ribbon Bold toggle does not light up. When the caret moves into a bullet list, her bullet toggle stays in its default state. The ribbon feels dead - operators have to look at the content to know what formatting applies, instead of letting the toolbar tell them.

Office-style ribbon with custom Bold and Italic toggles correctly reflecting the caret context

Tara goes hunting in the editor's public surface and finds two things she needs. The first is the SelectionChanged routed event on WpfHtmlEditor - it fires whenever the caret moves or the selection changes. The second is the StateQuery service, which answers exactly the questions she has: IsBold(), IsItalic(), IsUnorderedList(), and a dozen more.

She hooks the event in the editor's Loaded handler and pushes the state into her view model. Her ribbon toggles are already bound to view-model properties, so updating those properties is enough to make the toggles reflect reality.

private void ContractEditor_OnLoaded(object sender, RoutedEventArgs e)

{

    ContractEditor.SelectionChanged += ContractEditor_SelectionChanged;

}



private void ContractEditor_SelectionChanged(object sender, RoutedEventArgs e)

{

    var state = ContractEditor.StateQuery;

    var vm    = (RibbonViewModel)DataContext;



    vm.IsBold        = state.IsBold();

    vm.IsItalic      = state.IsItalic();

    vm.IsUnderline   = state.IsUnderline();

    vm.IsBulletList  = state.IsUnorderedList();

    vm.IsNumberList  = state.IsOrderedList();

    vm.IsAlignLeft   = state.IsJustifyLeft();

    vm.IsAlignCenter = state.IsJustifyCenter();

    vm.IsAlignRight  = state.IsJustifyRight();



    vm.ActiveFontFamily = state.GetActiveFontFamilyName();

    vm.ActiveFontSize   = state.GetActiveFontSize();



    vm.CanUndo  = state.CanUndo();

    vm.CanRedo  = state.CanRedo();

    vm.CanPaste = state.CanPaste();

}

The ribbon toggles in her XAML are bound to IsChecked="{Binding IsBold}", IsChecked="{Binding IsItalic}", and so on. The Undo and Redo ribbon buttons are bound to IsEnabled="{Binding CanUndo}" and IsEnabled="{Binding CanRedo}". As long as RibbonViewModel raises PropertyChanged on each setter, every toggle and button reflects the caret state automatically.

MVVM-bound ribbon Bold and Bullet List toggles activating as the caret enters formatted content

The first lawyer to test the build comments on it within five minutes: "Oh good, the bold thing actually works." Tara doesn't bother explaining how trivial the change was - she just smiles and adds it to the release notes.

The StateQuery service exposes a deep menu of context questions. Beyond the formatting toggles Tara wired, she has IsStrikeThrough(), IsSubscript(), IsSuperscript(), IsHyperLink(), IsImage(), IsTable(), IsTableCell(), IsActiveOrAncestorElementHyperLink(), IsActiveOrAncestorElementTable(), GetActiveForeColor(), GetActiveHighlightColor(), GetActiveHeaderTitleNumber(), IsActiveElementHeaderTitle(), CanCut(), CanCopy(), and CanDelete() available for future ribbon features without touching the event-subscription pattern at all.

  • Hook the routed SelectionChanged event on WpfHtmlEditor to be notified whenever the caret or selection moves.
  • Inside the handler, call editor.StateQuery.IsBold(), .IsItalic(), .IsUnorderedList(), etc. to learn the caret context.
  • Push the values into your view-model properties so any bound ToggleButton.IsChecked and IsEnabled binding updates automatically.
Last updated on May 15, 2026