File: System\Windows\Documents\WinRTSpellerInterop.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationFramework\PresentationFramework.csproj (PresentationFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using MS.Internal.WindowsRuntime.Windows.Data.Text;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Documents.Tracing;
using System.Windows.Input;
using System.Windows.Threading;
 
using System.Windows.Documents.MsSpellCheckLib;
 
//
// Description: Custom COM marshalling code and interfaces for interaction
//                  with the WinRT wordbreaker API and ISpellChecker
//                  spell-checker API
//
 
namespace System.Windows.Documents
{
    internal partial class WinRTSpellerInterop: SpellerInteropBase
    {
        #region Constructors
 
        /// <exception cref="PlatformNotSupportedException">
        /// The OS platform is not supported
        /// </exception>
        /// <exception cref="NotSupportedException">
        /// The OS platform is supportable, but spellchecking services are currently unavailable
        /// </exception>
        internal WinRTSpellerInterop()
        {
            try
            {
                SpellCheckerFactory.Create(shouldSuppressCOMExceptions: false);
            }
            catch (Exception ex)
                // Sometimes, InvalidCastException is thrown when SpellCheckerFactory fails to instantiate correctly
                when (ex is InvalidCastException || ex is COMException )
            {
                Dispose();
                throw new PlatformNotSupportedException(string.Empty, ex);
            }
 
            _spellCheckers = new Dictionary<CultureInfo, Tuple<WordsSegmenter, SpellChecker>>();
            _customDictionaryFiles = new Dictionary<string, List<string>>();
 
            _defaultCulture = InputLanguageManager.Current?.CurrentInputLanguage ?? Thread.CurrentThread.CurrentCulture;
            _culture = null;
 
            try
            {
                EnsureWordBreakerAndSpellCheckerForCulture(_defaultCulture, throwOnError: true);
            }
            catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PlatformNotSupportedException)
            {
                _spellCheckers = null;
                Dispose();
 
                if ((ex is PlatformNotSupportedException) || (ex is NotSupportedException))
                {
                    throw;
                }
                else
                {
                    throw new NotSupportedException(string.Empty, ex);
                }
            }
 
            // This class is only instantiated from the UI thread, so it is safe to
            // obtain the Dispatcher this way.
            _dispatcher = new WeakReference<Dispatcher>(Dispatcher.CurrentDispatcher);
 
            WeakEventManager<AppDomain, UnhandledExceptionEventArgs>
                .AddHandler(AppDomain.CurrentDomain, "UnhandledException", ProcessUnhandledException);
        }
 
        ~WinRTSpellerInterop()
        {
            Dispose(false);
        }
 
        #endregion Constructors
 
        #region IDispose
 
        public override void  Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        /// <summary>
        /// Internal interop resource cleanup
        /// </summary>
        /// <param name="disposing">
        ///     False when called from the Finalizer
        ///     True when called explicitly from Dispose()
        /// </param>
        protected override void Dispose(bool disposing)
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException(SR.TextEditorSpellerInteropHasBeenDisposed);
            }
 
            try
            {
                // Ensure that Dispose is called from the UI thread
                // If it is not, then make it so
                if (BeginInvokeOnUIThread((Action<bool>)Dispose, DispatcherPriority.Normal, disposing) == null)
                {
                    // Already on UI thread
                    // Continue with core Dispose logic
 
                    ReleaseAllResources(disposing);
                    _isDisposed = true;
                }
            }
            catch (InvalidOperationException)
            {
                // We have no way determining whether or not this is running
                // on the UI thread.
            }
        }
 
        #endregion
 
        #region Internal Methods
 
        internal override void SetLocale(CultureInfo culture)
        {
            Culture = culture;
        }
 
        /// <summary>
        /// Sets the mode in which the spell-checker operates
        /// We care about 3 different modes here:
        ///
        /// 1. Shallow spellchecking - i.e., wordbreaking +      spellchecking + NOT (suggestions)
        /// 2. Deep spellchecking    - i.e., wordbreaking +      spellchecking +      suggestions
        /// 3. Wordbreaking only     - i.e., wordbreaking + NOT (spellchcking) + NOT (suggestions)
        /// </summary>
        internal override SpellerMode Mode
        {
            set
            {
                _mode = value;
            }
        }
 
        /// <summary>
        /// If true, multi-word spelling errors would be detected
        /// This flag is ignored by WinRTSpellerInterop
        /// </summary>
        internal override bool MultiWordMode
        {
            set
            {
                // do nothing - multi-word mode specification is not supported
                // _multiWordMode = value;
            }
        }
 
        /// <summary>
        /// Sets spelling reform mode
        /// WinRTSpellerInterop doesn't support spelling reform
        /// </summary>
        /// <param name="culture"></param>
        /// <param name="spellingReform"></param>
        internal override void SetReformMode(CultureInfo culture, SpellingReform spellingReform)
        {
            // Do nothing - spelling reform is not supported
            // _spellingReformInfos[culture] =  spellingReform;
        }
 
        /// <summary>
        /// Returns true if we have an engine capable of proofing the specified language.
        /// </summary>
        /// <param name="culture"></param>
        /// <returns></returns>
        internal override bool CanSpellCheck(CultureInfo culture)
        {
            return !_isDisposed && EnsureWordBreakerAndSpellCheckerForCulture(culture);
        }
 
 
        #region Dictionary Methods
 
        /// <summary>
        /// Unloads a given custom dictionary
        /// </summary>
        /// <param name="token"></param>
        internal override void UnloadDictionary(object token)
        {
            if (_isDisposed) return;
 
            var data = (Tuple<string, string>)token;
            string ietfLanguageTag = data.Item1;
            string filePath = data.Item2;
 
            using (new SpellerCOMActionTraceLogger(this, SpellerCOMActionTraceLogger.Actions.UnregisterUserDictionary))
            {
                SpellCheckerFactory.UnregisterUserDictionary(filePath, ietfLanguageTag);
            }
 
            FileHelper.DeleteTemporaryFile(filePath);
        }
 
        /// <summary>
        /// Loads a custom dictionary
        /// </summary>
        /// <param name="lexiconFilePath"></param>
        /// <returns></returns>
        internal override object LoadDictionary(string lexiconFilePath)
        {
            return _isDisposed ? null : LoadDictionaryImpl(lexiconFilePath);
        }
 
        /// <summary>
        /// Loads a custom dictionary
        /// </summary>
        /// <param name="item"></param>
        /// <param name="trustedFolder"></param>
        /// <param name="dictionaryLoadedCallback"></param>
        /// <returns></returns>
        internal override object LoadDictionary(Uri item, string trustedFolder)
        {
            if (_isDisposed)
            {
                return null;
            }
 
            return LoadDictionaryImpl(item.LocalPath);
        }
 
        /// <summary>
        /// Releases all currently loaded custom dictionaries
        /// </summary>
        internal override void ReleaseAllLexicons()
        {
            if (!_isDisposed)
            {
                ClearDictionaries();
            }
        }
 
        #endregion
 
        #endregion Internal Methods
 
 
        #region Private Methods
 
        /// <summary>
        /// </summary>
        /// <param name="culture"></param>
        /// <param name="throwOnError"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private bool EnsureWordBreakerAndSpellCheckerForCulture(CultureInfo culture, bool throwOnError = false)
        {
            if (_isDisposed || (culture == null))
            {
                return false;
            }
 
            if(!_spellCheckers.ContainsKey(culture))
            {
                WordsSegmenter wordBreaker = null;
 
                try
                {
                    // Generally, we want to use the neutral language segmenter. This will ensure that the
                    // WordsSegmenter instance will not inadvertently de-compound words into stems. For e.g.,
                    // the dedicated segmenter for German will break down words like Hausnummer into {Haus, nummer},
                    // whereas the nuetral segmenter will not do so.
                    wordBreaker = WordsSegmenter.Create(culture.Name, shouldPreferNeutralSegmenter:true);
                }
                catch when (!throwOnError)
                {
                    // ArgumentException: culture name is malformed - unlikely given we use culture.Name
                    // PlatformNotSupportedException: OS is not supported
                    // NotSupportedException: culture name is likely well-formed, but not available currently for wordbreaking
                    wordBreaker = null;
                }
 
                // Even if wordBreaker.ResolvedLanguage == WordsSegmenter.Undetermined, we will use it
                // as an appropriate fallback wordbreaker as long as a corresponding ISpellChecker is found.
                if (wordBreaker == null)
                {
                    _spellCheckers[culture] = null;
                    return false;
                }
 
                SpellChecker spellChecker = null;
 
                try
                {
                    using (new SpellerCOMActionTraceLogger(this, SpellerCOMActionTraceLogger.Actions.SpellCheckerCreation))
                    {
                        spellChecker = new SpellChecker(culture.Name);
                    }
                }
                catch (Exception ex)
                {
                    spellChecker = null;
 
                    // ArgumentException:
                    // Either the language name is malformed (unlikely given we use culture.Name)
                    //   or this language is not supported. It might be supported if the appropriate
                    //   input language is added by the user, but it is not available at this time.
 
                    if (throwOnError && ex is ArgumentException)
                    {
                        throw new NotSupportedException(string.Empty, ex);
                    }
                }
 
                if (spellChecker == null)
                {
                    _spellCheckers[culture] = null;
                }
                else
                {
                    _spellCheckers[culture] = new Tuple<WordsSegmenter, SpellChecker>(wordBreaker, spellChecker);
                }
            }
 
            return (_spellCheckers[culture] == null ? false : true);
        }
 
        /// <summary>
        /// foreach(sentence in text.sentences)
        ///      foreach(segment in sentence)
        ///          continueIteration = segmentCallback(segment, data)
        ///      endfor
        ///
        ///      if (sentenceCallback != null)
        ///          continueIteration = sentenceCallback(sentence, data)
        ///      endif
        ///
        ///      if (!continueIteration)
        ///          break
        ///      endif
        ///  endfor
        /// </summary>
        /// <param name="text"></param>
        /// <param name="count"></param>
        /// <param name="sentenceCallback"></param>
        /// <param name="segmentCallback"></param>
        /// <param name="data"></param>
        /// <returns></returns>
        internal override int EnumTextSegments(char[] text, int count,
            EnumSentencesCallback sentenceCallback, EnumTextSegmentsCallback segmentCallback, object data)
        {
            if (_isDisposed)
            {
                return 0;
            }
 
            var wordBreaker = CurrentWordBreaker ?? DefaultCultureWordBreaker;
            var spellChecker = CurrentSpellChecker;
 
            bool spellCheckerNeeded = _mode.HasFlag(SpellerMode.SpellingErrors) || _mode.HasFlag(SpellerMode.Suggestions);
            if ((wordBreaker == null) || (spellCheckerNeeded && spellChecker == null)) return 0;
 
            int segmentCount = 0;
            bool continueIteration = true;
 
            // WinRT WordsSegmenter doesn't have the ability to break down text into segments (sentences).
            // Treat the whole text as a single segment for now.
            foreach(string strSentence in new string[]{string.Join(string.Empty, text)})
            {
                SpellerSentence sentence = new SpellerSentence(strSentence, wordBreaker, CurrentSpellChecker, this);
                segmentCount += sentence.Segments.Count;
 
                if (segmentCallback != null)
                {
                    for (int i = 0; continueIteration && (i < sentence.Segments.Count); i++)
                    {
                        continueIteration = segmentCallback(sentence.Segments[i], data);
                    }
                }
 
                if (sentenceCallback != null)
                {
                    continueIteration = sentenceCallback(sentence, data);
                }
 
                if (!continueIteration) break;
            }
 
            return segmentCount;
        }
 
        /// <summary>
        ///     Actual implementation of loading a dictionary
        /// </summary>
        /// <param name="lexiconFilePath"></param>
        /// <param name="dictionaryLoadedCallback"></param>
        /// <param name="callbackParam"></param>
        /// <returns>
        ///     A tuple of cultureinfo detected from <paramref name="lexiconFilePath"/> and
        ///     a temp file path which holds a copy of <paramref name="lexiconFilePath"/>
        ///
        ///     If no culture is specified in the first line of <paramref name="lexiconFilePath"/>
        ///     in the format #LID nnnn (where nnnn = decimal LCID of the culture), then invariant
        ///     culture is returned.
        /// </returns>
        /// <remarks>
        ///     At the end of this method, we guarantee that <paramref name="lexiconFilePath"/>
        ///     can be reclaimed (i.e., potentially deleted) by the caller.
        /// </remarks>
        private Tuple<string, string> LoadDictionaryImpl(string lexiconFilePath)
        {
            if (_isDisposed)
            {
                return new Tuple<string, string>(null, null);
            }
 
            if (!File.Exists(lexiconFilePath))
            {
                throw new ArgumentException(SR.Format(SR.CustomDictionaryFailedToLoadDictionaryUri, lexiconFilePath));
            }
 
            bool fileCopied = false;
            string lexiconPrivateCopyPath = null;
 
            try
            {
                CultureInfo culture = null;
 
                // Read the first line of the file and detect culture, if specified
                using (FileStream stream = new FileStream(lexiconFilePath, FileMode.Open, FileAccess.Read))
                {
                    string line = null;
                    using (StreamReader reader = new StreamReader(stream))
                    {
                        line = reader.ReadLine();
                        culture = WinRTSpellerInterop.TryParseLexiconCulture(line);
                    }
                }
 
                string ietfLanguageTag = culture.IetfLanguageTag;
 
                // Make a temp file and copy the original file over.
                // Ensure that the copy has Unicode (UTF16-LE) encoding
                using (FileStream lexiconPrivateCopyStream = FileHelper.CreateAndOpenTemporaryFile(out lexiconPrivateCopyPath, extension: "dic"))
                {
                    WinRTSpellerInterop.CopyToUnicodeFile(lexiconFilePath, lexiconPrivateCopyStream);
                    fileCopied = true;
                }
 
                // Add the temp file (with .dic extension) just created to a cache,
                // then pass it along to IUserDictionariesRegistrar
 
                if (!_customDictionaryFiles.ContainsKey(ietfLanguageTag))
                {
                    _customDictionaryFiles[ietfLanguageTag] = new List<string>();
                }
 
                _customDictionaryFiles[ietfLanguageTag].Add(lexiconPrivateCopyPath);
 
                using (new SpellerCOMActionTraceLogger(this, SpellerCOMActionTraceLogger.Actions.RegisterUserDictionary))
                {
                    SpellCheckerFactory.RegisterUserDictionary(lexiconPrivateCopyPath, ietfLanguageTag);
                }
 
                return new Tuple<string, string>(ietfLanguageTag, lexiconPrivateCopyPath);
            }
            catch (Exception e) when ((e is ArgumentException) || !fileCopied)
            {
                // IUserDictionariesRegistrar.RegisterUserDictionary can
                // throw ArgumentException on failure. Cleanup the temp file if
                // we successfully created one.
                if (lexiconPrivateCopyPath != null)
                {
                    FileHelper.DeleteTemporaryFile(lexiconPrivateCopyPath);
                }
 
                throw new ArgumentException(SR.Format(SR.CustomDictionaryFailedToLoadDictionaryUri, lexiconFilePath), e);
            }
        }
 
        /// <summary>
        ///     Actual implementation of clearing all dictionaries
        /// </summary>
        /// <remarks>
        ///     ClearDictionaries() can be called from the following methods
        ///         Dispose(bool):              UI thread
        ///         ReleaseAllLexicons:         UI thread
        ///         ProcessUnhandledException:  UI thread
        ///
        ///     Even when Dispose or ProcessUnhandledException runs in a different thread,
        ///     for e.g., the Finalizer thread or an arbitrary exception handling thread
        ///     respectively, we ensure that ClearDictionaries is always called in the UI
        ///     thread by invoking it with help from the cached <see cref="Dispatcher"/>
        /// </remarks>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private void ClearDictionaries(bool disposing = false)
        {
            if (_isDisposed)
            {
                // Likely this platform is not supported - do not process further.
                return;
            }
 
            if (_customDictionaryFiles != null)
            {
                foreach (KeyValuePair<string, List<string>> items in _customDictionaryFiles)
                {
                    string ietfLanguageTag = items.Key;
                    foreach (string filePath in items.Value)
                        try
                        {
                            using (new SpellerCOMActionTraceLogger(this, SpellerCOMActionTraceLogger.Actions.UnregisterUserDictionary))
                            {
                                SpellCheckerFactory.UnregisterUserDictionary(filePath, ietfLanguageTag);
                            }
 
                            FileHelper.DeleteTemporaryFile(filePath);
                        }
                        catch
                        {
                            // Do nothing - Continue to make a best effort
                            // attempt at unregistering custom dictionaries
                        }
                }
 
                _customDictionaryFiles.Clear();
            }
 
            if (disposing)
            {
                _customDictionaryFiles = null;
            }
        }
 
        /// <summary>
        ///     Detect whether the <paramref name="line"/> is of the form #LID nnnn,
        ///     and if it is, try to instantiate a CultureInfo object with LCID nnnn.
        /// </summary>
        /// <param name="line"></param>
        /// <returns>
        ///     The CultureInfo object corresponding to the LCID specified in the <paramref name="line"/>
        /// </returns>
        private static CultureInfo TryParseLexiconCulture(string line)
        {
            const string regexPattern = @"\s*\#LID\s+(\d+)\s*";
            RegexOptions regexOptions = RegexOptions.Singleline | RegexOptions.CultureInvariant | RegexOptions.Compiled;
 
            CultureInfo result = CultureInfo.InvariantCulture;
 
            if (line == null)
            {
                return result;
            }
 
            string[] matches = Regex.Split(line.Trim(), regexPattern, regexOptions);
 
            // We expect 1 exact match, which implies matches.Length == 3 (before, match, after)
            if (matches.Length != 3)
            {
                return result;
            }
 
            string before = matches[0];
            string match  = matches[1];
            string after  = matches[2];
 
            // We expect 1 exact match, which implies the following:
            //      before == after == string.Emtpy
            //      match is parsable into an integer
            int lcid;
            if ((before != string.Empty) || (after != string.Empty) || (!Int32.TryParse(match, out lcid)))
            {
                return result;
            }
 
            try
            {
                result = new CultureInfo(lcid);
            }
            catch (CultureNotFoundException)
            {
                result = CultureInfo.InvariantCulture;
            }
 
            return result;
        }
 
        /// <summary>
        ///     Copies <paramref name="sourcePath"/> to <paramref name="targetPath"/>. During the copy, it transcodes
        ///     <paramref name="sourcePath"/> to Unicode (UTL16-LE) if necessary and ensures that <paramref name="targetPath"/>
        ///     has the right BOM (Byte Order Mark) for UTF16-LE (FF FE)
        /// </summary>
        /// <see cref = "// See http://www.unicode.org/faq/utf_bom.html" />
        /// <param name="sourcePath"></param>
        /// <param name="targetPath"></param>
        private static void CopyToUnicodeFile(string sourcePath, FileStream targetStream)
        {
            bool utf16LEEncoding = false;
            using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read))
            {
                // Check that the first two bytes indicate the BOM for UTF16-LE
                // If found, we can directly copy the file over without additional transcoding.
                utf16LEEncoding = ((sourceStream.ReadByte() == 0xFF) && (sourceStream.ReadByte() == 0xFE));
                sourceStream.Seek(0, SeekOrigin.Begin);
 
                if (utf16LEEncoding)
                {
                    sourceStream.CopyTo(targetStream);
                }
                else
                {
                    using (StreamReader reader = new StreamReader(sourceStream))
                    {
                        // Create the StreamWriter with encoding = Unicode to ensure that the new file
                        // contains the BOM for UTF16-LE, and also ensures that the file contents are
                        // encoded correctly
                        using (StreamWriter writer = new StreamWriter(targetStream, Text.Encoding.Unicode))
                        {
                            string line = null;
                            while ((line = reader.ReadLine()) != null)
                            {
                                writer.WriteLine(line);
                            }
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Attempts to unregister all custom dictionaries if an unhandled exception is raised
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessUnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            var disposing = false;
 
            try
            {
                if (BeginInvokeOnUIThread((Action<bool>)ClearDictionaries, DispatcherPriority.Normal, disposing) == null)
                {
                    // Already on UI thread
                    ClearDictionaries(disposing);
                }
            }
            catch(InvalidOperationException)
            {
                // We have no way determining whether or not this is running
                // on the UI thread. Safest to avoid calling into thread-sensitive
                // operations like ClearDictionaries
            }
        }
 
 
        /// <summary>
        /// Releases all resources by:
        /// <list type="bullet">
        /// <item>disposing all IDisposables, which would in turn
        /// free up any associated respective native resources</item>
        /// <item>Unregistering all custom dictionaries</item>
        /// </list>
        /// </summary>
        /// <param name="disposing">True when called from <see cref="Dispose"/>, False when called from the Finalizer</param>
        private void ReleaseAllResources(bool disposing)
        {
            if (_spellCheckers != null)
            {
                foreach (Tuple<WordsSegmenter, SpellChecker> item in _spellCheckers.Values)
                {
                    SpellChecker spellChecker = item?.Item2;
                    if (spellChecker != null)
                    {
                        spellChecker.Dispose();
                    }
                }
 
                _spellCheckers = null;
            }
 
            ClearDictionaries(disposing);
        }
 
        /// <summary>
        /// Executes the specified delegate asynchronously on the UI thread using the <see cref="Dispatcher"/>
        /// stored in the <see cref="WeakReference{T}"/> <see cref="_dispatcher"/>
        ///
        /// If the current thread is already the UI thread, then the delegate is is not executed and the method
        /// returns null.
        /// </summary>
        /// <param name="method">The delegate to a method that takes parameters specified in <paramref name="args"/>,
        /// which is pushed onto the <see cref="Dispatcher"/> queue.</param>
        /// <param name="priority">The priority, relative to the other pending operations in the <see cref="Dispatcher"/>
        /// event queue, the specified method is invoked.</param>
        /// <param name="args">An array of objects to pass as arguments to the given method. Can be <code>null</code></param>
        /// <returns>
        /// An object, which is returned immediately after <see cref="Dispatcher.BeginInvoke"/> is called,
        /// that can be used to interact with the delegate as it is pending execution in the event queue.
        ///
        /// If the current thread is already the same thread as the one on which the cached <see cref="Dispatcher"/>
        /// is running, then it returns null.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Thrown when the cached <see cref="Dispatcher"/> can not be obtained
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// Thrown by <see cref="Dispatcher.BeginInvoke"/> when <paramref name="method"/> is <code>null
        /// </code></exception>
        /// <remarks>
        ///     The usual pattern for methods that deal with delegates is to mark them with
        ///     the <see cref="SecuritySafeCriticalAttribute"/>. This would ensure that the caller
        ///     is responsible for managing its CAS attribute appropriately, but the method
        ///     that receives the delegate and orchestrates its execution can do so successfully
        ///     irrespective of whether the delegate is Critical or Transparent.
        ///
        ///     In this instance, marking this method with <see cref="SecuritySafeCriticalAttribute"/> is
        ///     unnecessary because <see cref="Dispatcher.BeginInvoke"/> is really what is used to
        ///     orchestrate the execution of <paramref name="method"/>, and in turn, <see cref="Dispatcher.BeginInvoke"/>
        ///     takes care of marking its own CAS attribute appropriately.
        /// </remarks>
        private DispatcherOperation BeginInvokeOnUIThread(Delegate method, DispatcherPriority priority, params object[] args)
        {
            Dispatcher dispatcher = null;
 
            if (_dispatcher == null ||
                !_dispatcher.TryGetTarget(out dispatcher) ||
                dispatcher == null)
            {
                throw new InvalidOperationException();
            }
 
            if (!dispatcher.CheckAccess())
            {
                return dispatcher.BeginInvoke(method, priority, args);
            }
 
            return null;
        }
 
        #endregion
 
        #region Private Properties
 
        private CultureInfo Culture
        {
            get
            {
                return _culture;
            }
 
            set
            {
                _culture = value;
                EnsureWordBreakerAndSpellCheckerForCulture(_culture);
            }
        }
 
        private WordsSegmenter CurrentWordBreaker
        {
            get
            {
                if (Culture == null)
                {
                    return null;
                }
                else
                {
                    EnsureWordBreakerAndSpellCheckerForCulture(Culture);
                    return _spellCheckers[Culture]?.Item1;
                }
            }
        }
 
        private WordsSegmenter DefaultCultureWordBreaker
        {
            get
            {
                if (_defaultCulture == null)
                {
                    return null;
                }
                else
                {
                    return _spellCheckers[_defaultCulture]?.Item1;
                }
            }
        }
 
        private SpellChecker CurrentSpellChecker
        {
            get
            {
                if (Culture == null)
                {
                    return null;
                }
                else
                {
                    EnsureWordBreakerAndSpellCheckerForCulture(Culture);
                    return _spellCheckers[Culture]?.Item2;
                }
            }
        }
 
        #endregion
 
        #region Private Fields
 
        private bool _isDisposed = false;
        private SpellerMode _mode = SpellerMode.None;
 
        // Cache of word-breakers and spellcheckers
        private Dictionary<CultureInfo, Tuple<WordsSegmenter, SpellChecker>> _spellCheckers;
 
        private CultureInfo _defaultCulture;
        private CultureInfo _culture;
 
        /// <summary>
        /// Cache of private dictionaries
        /// Key: ietfLanguageTag
        /// Values: List of file names that have been registered for <i>ietfLanguageTag</i>
        /// </summary>
        private Dictionary<string, List<string>> _customDictionaryFiles;
 
        /// <summary>
        /// <see cref="Dispatcher"/> associated with the thread on which this instance
        /// of <see cref="WinRTSpellerInterop"/> was created. This <see cref="Dispatcher"/>
        /// is used to ensure that all COM calls are delegated to the UI thread
        /// by the finalizer.
        /// </summary>
        private readonly WeakReference<Dispatcher> _dispatcher;
 
        #endregion Private Fields
 
        #region Internal Types
 
        internal readonly struct TextRange: SpellerInteropBase.ITextRange
        {
            public TextRange(MS.Internal.WindowsRuntime.Windows.Data.Text.TextSegment textSegment)
            {
                _length = (int)textSegment.Length;
                _start = (int)textSegment.StartPosition;
            }
 
            public TextRange(int start, int length)
            {
                _start = start;
                _length = length;
            }
 
            public TextRange(ITextRange textRange) : 
                this(textRange.Start, textRange.Length)
            {
            }
 
            public static explicit operator TextRange(MS.Internal.WindowsRuntime.Windows.Data.Text.TextSegment textSegment)
            {
                return new TextRange(textSegment);
            }
 
            #region SpellerInteropBase.ITextRange
 
            public int Start
            {
                get { return _start;  }
            }
 
            public int Length
            {
                get { return _length; }
            }
 
            #endregion
 
            private readonly int _start;
            private readonly int _length;
        }
 
        [DebuggerDisplay("SubSegments.Count = {SubSegments.Count} TextRange = {TextRange.Start},{TextRange.Length}")]
        internal class SpellerSegment: ISpellerSegment
        {
            #region Constructor
 
            public SpellerSegment(string sourceString, ITextRange textRange, SpellChecker spellChecker, WinRTSpellerInterop owner)
            {
                _spellChecker = spellChecker;
                _suggestions = null;
                Owner = owner;
 
                SourceString = sourceString;
                TextRange = textRange;
            }
 
            static SpellerSegment()
            {
                _empty = new List<ISpellerSegment>().AsReadOnly();
            }
 
            #endregion
 
            #region Private Methods
 
            private void EnumerateSuggestions()
            {
                List<string> result = new List<string>();
                _isClean = true;
 
                if (_spellChecker == null)
                {
                    _suggestions = result.AsReadOnly();
                    return;
                }
 
                List<SpellChecker.SpellingError> spellingErrors = null;
 
                using (new SpellerCOMActionTraceLogger(Owner, SpellerCOMActionTraceLogger.Actions.ComprehensiveCheck))
                {
                    spellingErrors = Text != null ? _spellChecker.ComprehensiveCheck(Text) : null;
                }
 
                if (spellingErrors == null)
                {
                    _suggestions = result.AsReadOnly();
                    return;
                }
 
                foreach (var spellingError in spellingErrors)
                {
                    result.AddRange(spellingError.Suggestions);
                    if (spellingError.CorrectiveAction != SpellChecker.CorrectiveAction.None)
                    {
                        _isClean = false;
                    }
                }
 
                _suggestions = result.AsReadOnly();
            }
 
            #endregion
 
            #region SpellerInteropBase.ISpellerSegment
 
            /// <summary>
            /// <inheritdoc/>
            /// </summary>
            public string SourceString { get; }
 
            /// <summary>
            /// <inheritdoc/>
            /// </summary>
            public string Text => SourceString?.Substring(TextRange.Start, TextRange.Length);
 
            /// <summary>
            /// Returns a read-only list of sub-segments of this segment
            /// WinRT word-segmenter doesn't really support sub-segments,
            ///   so we always return an empty list
            /// </summary>
            public IReadOnlyList<ISpellerSegment> SubSegments
            {
                get
                {
                    return SpellerSegment._empty;
                }
            }
 
            public ITextRange TextRange { get; }
 
            public IReadOnlyList<string> Suggestions
            {
                get
                {
                    if (_suggestions == null)
                    {
                        EnumerateSuggestions();
                    }
 
                    return _suggestions;
                }
            }
 
            public bool IsClean
            {
                get
                {
                    if (_isClean == null)
                    {
                        EnumerateSuggestions();
                    }
 
                    return _isClean.Value;
                }
            }
 
            /// <remarks>
            /// This field is used only to support TraceLogging telemetry
            /// logged using <see cref="SpellerCOMActionTraceLogger"/>. It
            /// has no other functional use.
            /// </remarks>
            internal WinRTSpellerInterop Owner { get; }
 
            public void EnumSubSegments(EnumTextSegmentsCallback segmentCallback, object data)
            {
                bool result = true;
 
                for (int i = 0; result && (i < SubSegments.Count); i++)
                {
                    result = segmentCallback(SubSegments[i], data);
                }
            }
 
            #endregion SpellerInteropBase.ISpellerSegment
 
            #region Private Fields
 
 
            SpellChecker _spellChecker;
            private IReadOnlyList<string> _suggestions;
            private bool? _isClean = null;
 
            private static readonly IReadOnlyList<ISpellerSegment> _empty;
 
            #endregion Private Fields
        }
 
        #endregion Internal Types
 
        #region Private Types
 
        [DebuggerDisplay("Sentence = {_sentence}")]
        private class SpellerSentence: ISpellerSentence
        {
            public SpellerSentence(string sentence, WordsSegmenter wordBreaker, SpellChecker spellChecker, WinRTSpellerInterop owner)
            {
                _sentence = sentence;
                _wordBreaker = wordBreaker;
                _spellChecker = spellChecker;
                _segments = null;
                _owner = owner;
            }
 
            #region SpellerInteropBase.ISpellerSentence
 
            public IReadOnlyList<ISpellerSegment> Segments
            {
                get
                {
                    if (_segments == null)
                    {
                        _segments = _wordBreaker.ComprehensiveGetTokens(_sentence, _spellChecker, _owner);
                    }
 
                    return _segments;
                }
            }
 
            public int EndOffset
            {
                get
                {
                    int endOffset = -1;
 
                    if (Segments.Count > 0)
                    {
                        ITextRange textRange = Segments[Segments.Count - 1].TextRange;
                        endOffset = textRange.Start + textRange.Length;
                    }
 
                    return endOffset;
                }
            }
 
            #endregion
 
            private string _sentence;
            private WordsSegmenter _wordBreaker;
            private SpellChecker  _spellChecker;
            private IReadOnlyList<SpellerSegment> _segments;
 
            /// <remarks>
            /// This field is used only to support TraceLogging telemetry
            /// logged using <see cref="SpellerCOMActionTraceLogger"/>. It
            /// has no other functional use.
            /// </remarks>
            private WinRTSpellerInterop _owner;
        }
 
        #endregion Private Types
 
        #region Private Interfaces
 
        #endregion Private Interfaces
    }
}