Using a Custom Spell-Check Engine

Daniel writes desktop software for a clinical-trials company. Their drug-naming database is a curated list of seventy thousand compound names, brand candidates, and pharmacological abbreviations that the regulatory team updates twice a week. When a clinician types a trial report inside the company's desktop app, the editor must accept those seventy thousand terms as correct and flag the ones outside the list. No off-the-shelf English dictionary will ever know what "Briletumab-7XQ" is.

The built-in spell checker is excellent at general English. It is not, and cannot reasonably be, an expert on Daniel's domain. So Daniel takes over the spell-check pipeline entirely. The editor exposes a five-method interface, ISpellCheckerEngine, and once he hands his own implementation to the control every Spell, Suggest, and Add call -- inline squiggles and dialog mode alike -- is routed through his code instead.

Architecture diagram showing WinFormHtmlEditor delegating Spell and Suggest calls through a custom ISpellCheckerEngine implementation to an external terminology service

The interface he has to implement

Five methods. The editor never calls anything else.

namespace SpiceLogic.HtmlEditor.Abstractions.Entities.SpellCheck;



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();

}

Initialize fires once, the first time the editor needs to spell-check anything. The three path arguments are whatever Daniel set on SpellCheckOptions.DictionaryFile; in his case the engine ignores them entirely because his vocabulary lives in a REST service. Spell returns true when the word is acceptable, false when the editor should flag it. Suggest returns ranked alternatives, honouring the max hint that the control passes through from MaxSuggestionsForInlineChecking and MaxSuggestionsForDialogs. AddToUserDictionary is called when an end user right-clicks a flagged word and picks "Add to Dictionary." Dispose is the cleanup hook for HTTP clients, file handles, or in-process indexes.

Spell is the hot path. The editor calls it once per word on every spell-check pass, so Daniel caches aggressively -- a memory-resident HashSet<string> backed by a fifteen-minute timer that refreshes from the regulatory service.

The engine

using System.Collections.Generic;

using System.Linq;

using SpiceLogic.HtmlEditor.Abstractions.Entities.SpellCheck;



public sealed class ClinicalTermsEngine : ISpellCheckerEngine

{

    private readonly TerminologyServiceClient _service;

    private HashSet<string> _approvedTerms = new(StringComparer.OrdinalIgnoreCase);



    public ClinicalTermsEngine(TerminologyServiceClient service)

    {

        _service = service;

    }



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

    {

        // Paths come from SpellCheckOptions.DictionaryFile. This engine ignores them

        // because the source of truth is a server-side regulatory database.

        _approvedTerms = new HashSet<string>(_service.DownloadVocabulary(), StringComparer.OrdinalIgnoreCase);

    }



    public bool Spell(string word) => _approvedTerms.Contains(word);



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

    {

        var hits = _service.FindSimilar(word, max ?? 5);

        return max.HasValue ? hits.Take(max.Value) : hits;

    }



    public void AddToUserDictionary(string word)

    {

        _service.SubmitForReview(word);  // Regulatory queue, not a local file

        _approvedTerms.Add(word);

    }



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

}

Plugging it in

Two assignments at startup and the editor uses the new engine for both inline squiggles and the dialog walk-through. The SpellChecker selection on the control is the switch that tells the control "use the custom plug-in I just gave you instead of the built-in one":

public partial class TrialReportForm : Form

{

    public TrialReportForm(TerminologyServiceClient service)

    {

        InitializeComponent();



        editor.SpellCheckOptions.CustomSpellCheckerEngine = new ClinicalTermsEngine(service);

        editor.SpellChecker                               = SpellCheckerEngineTypes.Custom;



        editor.SpellCheckOptions.FireInlineSpellCheckingOnKeyStroke = true;

    }

}

From here on, every word in the editor is checked against the regulatory service. "Briletumab-7XQ" no longer carries a red squiggle. "Briletumab-7XR," which does not exist in the regulatory database, does.

Trial report editor with industry-specific compound names accepted as valid while an unknown variant is flagged

When the simple engine isn't enough

Daniel's first deployment worked, but two refinements followed once real clinicians started using it. The regulatory service occasionally goes down for maintenance, and a network blip should not make every word in a report light up red. He wraps Spell in a fail-open fallback: when the service is unreachable, the engine returns true and the report is treated as clean until the connection recovers. The second refinement was performance: the daily report runs in the thirty-thousand-word range, and his initial implementation made one HTTP call per unknown word for suggestions. The fix was to cache suggestions for the most recent few hundred mis-hits per session and to honour the max hint scrupulously -- inline suggestions ask for three, the dialog asks for ten.

Patterns this same plug-in serves

Daniel's clinical-terms story is one shape; the same interface answers several others. A corporate authoring team can route every spell call through their company terminology server. A legal-tech vendor can plug in a contracts-and-citation dictionary. An email-AI startup can hand the editor a backend-driven engine that does grammar, style, and tone in addition to spelling. None of these need to ship a Hunspell binary, and none of them give up the editor's inline squiggle UI -- the same engine drives both modes the moment SpellChecker = SpellCheckerEngineTypes.Custom is set.

Sample to start from

A runnable end-to-end sample lives in the product download. Look for the Custom Spell Checker project under the installation's Samples folder -- it implements ISpellCheckerEngine against a small in-memory vocabulary and wires it into a WinFormHtmlEditor exactly as shown above.

Last updated on May 15, 2026