Using a Custom Spell-Check Engine

Marcus works on the clinical-trials platform at a mid-sized contract research organisation. His team has a WPF authoring tool where medical writers compose investigator brochures, and somewhere on page 14 of a typical brochure sits a word like pharmacokinetics, oligonucleotide, or angiotensin-converting. The built-in dictionary flags every one of them. Worse, the writers are not allowed to add words themselves -- the terminology list is owned by the medical writing standards group and lives in a SharePoint-backed REST service that returns approved terms for the trial in scope.

Marcus needs the editor to ask his service, not its own dictionary, whether angiotensin-converting is correct. The hook for that is a single interface in SpiceLogic.HtmlEditor.Abstractions.Entities.SpellCheck called ISpellCheckerEngine. Implement it, hand an instance to the editor, and every spell decision -- inline squiggle, dialog suggestion, right-click menu -- routes to his code.

What the interface asks for

Four methods, plus Dispose. Initialize is called once with the dictionary, affix, and user-dictionary file paths configured on the editor -- Marcus ignores them and connects to his REST service instead. Spell answers true or false for a single word. Suggest returns the alternatives a user sees on right-click. AddToUserDictionary is invoked when the user picks Add to Dictionary; Marcus routes that to a "propose new term" workflow rather than a local file.

public interface ISpellCheckerEngine

{

    void Initialize(string dictionaryPath, string affixPath, string userDictionaryPath);

    bool Spell(string word);

    IEnumerable<string> Suggest(string word, int? max = null);

    void AddToUserDictionary(string word);

    void Dispose();

}

The implementation

Marcus writes a thin adapter over his terminology client. To keep the editor responsive while a writer types, he caches each yes/no decision for the lifetime of the session -- a writer who types pharmacokinetics 40 times in one brochure hits the network exactly once. Suggestions are looked up on demand because the right-click menu is opened deliberately.

public sealed class TrialsTerminologyEngine : ISpellCheckerEngine

{

    private readonly ITerminologyClient _client;

    private readonly ConcurrentDictionary<string, bool> _seen = new(StringComparer.OrdinalIgnoreCase);



    public TrialsTerminologyEngine(ITerminologyClient client) => _client = client;



    public void Initialize(string dictionaryPath, string affixPath, string userDictionaryPath)

    {

        // Paths are unused -- terminology comes from the service.

    }



    public bool Spell(string word)

        => _seen.GetOrAdd(word, w => _client.IsApproved(w));



    public IEnumerable<string> Suggest(string word, int? max = null)

        => _client.SuggestApproved(word, max ?? 5);



    public void AddToUserDictionary(string word)

    {

        _client.ProposeNewTerm(word);

        _seen[word] = true; // accept it locally until standards group rules

    }



    public void Dispose() => _client.Dispose();

}
WPF HTML editor running a custom ISpellCheckerEngine implementation, accepting clinical-trial terminology that the built-in dictionary would otherwise flag

Plugging it in

Two property assignments on the editor and the swap is complete. The first hands the editor the engine instance; the second tells the editor to actually call into it instead of the built-in:

BrochureEditor.SpellCheckOptions.CustomSpellCheckerEngine =

    new TrialsTerminologyEngine(_terminologyClient);

BrochureEditor.SpellCheckOptions.SpellChecker = SpellCheckerEngineTypes.Custom;

From that point on, the red squiggle in BrochureEditor reflects the standards group's decisions. The dialog walker uses the same engine. So does Add to Dictionary. Nothing in the editor's UI changes -- only the source of truth behind it.

When the engine cannot answer fast

Marcus's service is fast for cached words and slow on a cold lookup. Inline spell checking calls Spell on every word in the viewport, which means a fresh document could fire dozens of network calls in the same tick. Two settings absorb the worst of that. The cache in his implementation eliminates repeat lookups. The InlineSpellCheckDebounceMilliseconds setting on SpellCheckOptions (default 300 ms) ensures the editor waits for the writer to pause before kicking off a scan. For very large documents, leaving inline off and exposing only the dialog walker is a legitimate UX choice.

Shipping it

Marcus ships the adapter as part of the same WPF assembly that hosts the editor. No additional configuration on user machines. The terminology client points at production for QA and at a sandbox for development through the same DI container that wires the rest of the application. The sample project in the product download includes a working ISpellCheckerEngine implementation backed by an in-memory list -- a useful starting skeleton when the network-backed version is overkill.

Sample WPF solution showing the custom spell-check engine source files alongside the brochure editor window
Last updated on May 15, 2026