Spelling in 1C via COM in C#

Formulation of the problem

Hello everyone, I recently encountered a problem spell checks And typo corrections in 1C. Having looked at the possible solutions (MS Word, Yandex, etc.), I realized that they were not suitable for me. I decided to dig deeper. Personally, I liked the solution based on spell checker built into Windows. Since 1C does not have the ability to directly access this OS functionality, I implemented it as a DLL in C# and made a COM wrapper. I connected the COM object to 1C.

As a result, we got a simple form like this, where when you click the “Check typos” button, the text in the Input Line is analyzed and corrected.

The source code of the COM object, 1C processing and detailed explanations are given below.

You can download the source code here:

https://github.com/amizerov/SpellChecker

SpellCheck.epf

Binaries of the COM object are here:

https://mizer.dev/Files/SpellChecker.rar

Introduction

The task of checking spelling while entering text and finding typos can arise in any project where the user must manually enter some data, such as product names, training course items, etc.

Windows' built-in spell checker, described hereprovide interfaces that can be used to automatically detect and correct typos in texts. This is especially useful for developers who want to add spell checking functionality to their applications without resorting to third-party services and having to implement complex algorithms from scratch.

In this article, we'll look at how to use Windows COM (Component Object Model) interfaces to integrate a built-in spell checker into your C# projects.

I will show how to publish the error search functionality as your own COM component, and how to use it in the 1C processing code. Often 1C users try to solve this problem using SpellChecker MS Word, but sorry, it works much slower than the method described below.

SpellChecker Project in C# in Visual Studio 2022

To create a COM component in C#, we use a project template – Class Library (Microsoft)I named the project SpellChecker. Let's include a class in it SpellCheckerBasein which we simply declare all the Spell Checking interfaces – ha from the official Microsoft documentation https://learn.microsoft.com/en-us/windows/win32/intl/spell-checker-interfaces

SpellCheckerBase.cs
using System.Runtime.InteropServices;

namespace SpellChecker;

public class SpellCheckerBase
{
    protected enum CORRECTIVE_ACTION
    {
        CORRECTIVE_ACTION_NONE,
        CORRECTIVE_ACTION_GET_SUGGESTIONS,
        CORRECTIVE_ACTION_REPLACE,
        CORRECTIVE_ACTION_DELETE,
    }

    [Guid("B7C82D61-FBE8-4B47-9B27-6C0D2E0DE0A3")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComImport]
    protected interface ISpellingError
    {
        uint StartIndex { get; }
        uint Length { get; }
        CORRECTIVE_ACTION CorrectiveAction { get; }
        string Replacement { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
    }

    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("803E3BD4-2828-4410-8290-418D1D73C762")]
    [ComImport]
    protected interface IEnumSpellingError
    {
        [return: MarshalAs(UnmanagedType.Interface)]
        ISpellingError Next();
    }

    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("00000101-0000-0000-C000-000000000046")]
    [ComImport]
    protected interface IEnumString
    {
        void Next([In] uint celt, [MarshalAs(UnmanagedType.LPWStr)] out string rgelt, out uint pceltFetched);
        void Skip([In] uint celt);
        void Reset();
        void Clone([MarshalAs(UnmanagedType.Interface)] out IEnumString ppenum);
    }

    [Guid("432E5F85-35CF-4606-A801-6F70277E1D7A")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComImport]
    protected interface IOptionDescription
    {
        string Id { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
        string Heading { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
        string Description { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
        IEnumString Labels { [return: MarshalAs(UnmanagedType.Interface)] get; }
    }

    [Guid("0B83A5B0-792F-4EAB-9799-ACF52C5ED08A")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComImport]
    protected interface ISpellCheckerChangedEventHandler
    {
        void Invoke([MarshalAs(UnmanagedType.Interface), In] ISpellChecker sender);
    }

    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("B6FD0B71-E2BC-4653-8D05-F197E412770B")]
    [ComImport]
    protected interface ISpellChecker
    {
        string languageTag { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
        [return: MarshalAs(UnmanagedType.Interface)]
        IEnumSpellingError Check([MarshalAs(UnmanagedType.LPWStr), In] string text);
        [return: MarshalAs(UnmanagedType.Interface)]
        IEnumString Suggest([MarshalAs(UnmanagedType.LPWStr), In] string word);
        void Add([MarshalAs(UnmanagedType.LPWStr), In] string word);
        void Ignore([MarshalAs(UnmanagedType.LPWStr), In] string word);
        void AutoCorrect([MarshalAs(UnmanagedType.LPWStr), In] string from, [MarshalAs(UnmanagedType.LPWStr), In] string to);
        byte GetOptionValue([MarshalAs(UnmanagedType.LPWStr), In] string optionId);
        IEnumString OptionIds { [return: MarshalAs(UnmanagedType.Interface)] get; }
        string Id { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
        string LocalizedName { [return: MarshalAs(UnmanagedType.LPWStr)] get; }
        uint add_SpellCheckerChanged([MarshalAs(UnmanagedType.Interface), In] ISpellCheckerChangedEventHandler handler);
        void remove_SpellCheckerChanged([In] uint eventCookie);
        [return: MarshalAs(UnmanagedType.Interface)]
        IOptionDescription GetOptionDescription([MarshalAs(UnmanagedType.LPWStr), In] string optionId);
        [return: MarshalAs(UnmanagedType.Interface)]
        IEnumSpellingError ComprehensiveCheck([MarshalAs(UnmanagedType.LPWStr), In] string text);
    }

    [Guid("8E018A9D-2415-4677-BF08-794EA61F94BB")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComImport]
    protected interface ISpellCheckerFactory
    {
        IEnumString SupportedLanguages { [return: MarshalAs(UnmanagedType.Interface)] get; }
        int IsSupported([MarshalAs(UnmanagedType.LPWStr), In] string languageTag);
        [return: MarshalAs(UnmanagedType.Interface)]
        ISpellChecker CreateSpellChecker([MarshalAs(UnmanagedType.LPWStr), In] string languageTag);
    }

    [Guid("7AB36653-1796-484B-BDFA-E74F1DB7C1DC")]
    [ComImport]
    protected class SpellCheckerFactoryClass
    {
    }
}

Let's add a class to the project SpellCheckerAPIwhich we inherit from SpellCheckerBase and implement in it our only, main static method for spell checking SpellCheck with a result convenient for us in the form of a list of objects of the type SpellCheckResultFor brevity, it is declared a record.

public record SpellCheckResult(string Word, string Action, string Replacement, List Suggestions);

This is the list we will return Listthis is a list of all words with errors from the analyzed text.

Word here it is the word itself with a mistake found in the text, Action – one of the suggested actions (replace, delete or ignore), Replacement – the main word to replace the erroneous and Suggestions – list of all replacement variators.

SpellCheckerAPI.cs
using System.Runtime.InteropServices;

namespace SpellChecker;

public record SpellCheckResult(string Word, string Action, string Replacement, List<string> Suggestions);

public class SpellCheckerAPI : SpellCheckerBase
{
    public static List<SpellCheckResult> SpellCheck(string s)
    {
        SpellCheckerFactoryClass? factory = null;
        ISpellCheckerFactory? ifactory = null;
        ISpellChecker? checker = null;
        ISpellingError? error = null;
        IEnumSpellingError? errors = null;
        IEnumString? suggestions = null;

        List<SpellCheckResult> spellCheckResults = new();

        try
        {
            factory = new SpellCheckerFactoryClass();
            ifactory = (ISpellCheckerFactory)factory;

            //проверим поддержку русского языка
            int res = ifactory.IsSupported("ru-RU");
            if (res == 0) { throw new Exception("Fatal error: russian language not supported!"); }

            checker = ifactory.CreateSpellChecker("ru-RU");

            errors = checker.Check(s);
            while (true)
            {
                //получаем ошибку
                if (error != null) { Marshal.ReleaseComObject(error); error = null; }
                error = errors.Next();
                if (error == null) break;

                //получаем слово с ошибкой
                string word = s.Substring((int)error.StartIndex, (int)error.Length);
                string action = "";
                string replac = error.Replacement;
                List<string> sugges = new();

                //получаем рекомендуемое действие
                switch (error.CorrectiveAction)
                {
                    case CORRECTIVE_ACTION.CORRECTIVE_ACTION_DELETE:
                        action = "удалить";
                        break;

                    case CORRECTIVE_ACTION.CORRECTIVE_ACTION_REPLACE:
                        action = "заменить";
                        break;

                    case CORRECTIVE_ACTION.CORRECTIVE_ACTION_GET_SUGGESTIONS:
                        action = "заменить на одно из";

                        if (suggestions != null) { Marshal.ReleaseComObject(suggestions); suggestions = null; }

                        //получаем список слов, предложенных для замены
                        suggestions = checker.Suggest(word);

                        while (true)
                        {
                            string suggestion;
                            uint count = 0;
                            suggestions.Next(1, out suggestion, out count);
                            if (count == 1) sugges.Add(suggestion);
                            else break;
                        }
                        break;
                }

                if(replac == "") replac = sugges.Count > 0 ? sugges[0] : "";
                spellCheckResults.Add(new SpellCheckResult(word, action, replac, sugges));
            }
        }
        finally
        {
            if (suggestions != null) { Marshal.ReleaseComObject(suggestions); }
            if (factory != null) { Marshal.ReleaseComObject(factory); }
            if (ifactory != null) { Marshal.ReleaseComObject(ifactory); }
            if (checker != null) { Marshal.ReleaseComObject(checker); }
            if (error != null) { Marshal.ReleaseComObject(error); }
            if (errors != null) { Marshal.ReleaseComObject(errors); }
        }

        return spellCheckResults;
    }
}

Now, for all this, you need to make a COM wrapper, for this, in the same project namespace SpellCheckerlet's create another class ComService with attribute [ComVisible(true)] and assign it a unique Guid.

We implement a public method in it SpellCheckwhich will be accessible via COM. Let it simply return a JSON string with the results of the check, erroneous words, and suggestions for replacing erroneous words.

using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace SpellChecker;

[ComVisible(true)]
[Guid("fe103d6e-e71b-414c-80bf-982f18f23237")]
public class ComService
{
    public string SpellCheck(string text)
    {
        var spellCheckResults = SpellCheckerAPI.SpellCheck(text);
        string x = JsonSerializer.Serialize(spellCheckResults);
        string jsonString = Regex.Unescape(x);

        return jsonString;
    }
}

In the project description file, add the line truewhich will tell the compiler to add a COM wrapper for our library.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <EnableComHosting>true</EnableComHosting>
  </PropertyGroup>

</Project>

After this, when building the project, the compiler will generate an additional file SpellChecker.comhost.dll.

In it the compiler implements the method DllRegisterServerwhich will allow us to register our component in the registry using the regsvr32 utility.

This is what should be in the output folder after compilation:

It turns out something like this SpellChecker.comhost.dll here is the main one, but without SpellChecker.dll and SpellChecker.runtimeconfig.json, without these 2 files it does not work, it is just a wrapper, it still does all the useful work SpellChecker.dll

Registering a COM object

So, let's register the file SpellChecker.comhost.dll in the registry with the utility regsvr32from the command line launched with administrator rights.

You just need to run the command: regsvr32 SpellChecker.comhost.dll in the directory where our library is located. It is very important to note that the folder must contain 3 files:

  1. SpellCheck.comhost.dll

  2. SpellCheck.dll

  3. SpellCheck.runtimeconfig.json

Otherwise regsvr32 will return an error. SpellCheck.pdb and the second json files do not affect, but you can leave them as well.

If you did everything correctly, we will see a window with Successful object registration, and an entry for our new ProgID with the value will appear in the registry SpellChecker.ComService

1C processing

Now in the 1C code we can use the functionality implemented in our DLL, connecting the COM object in the usual manner, as they say, late binding:

srv = New COMObject(“SpellChecker.ComService”);

and then, calling the method srv.SpellCheckwe will get the result as a JSON string like this:

res = srv.SpellCheck(txt);

We will transform the result into an array of objects:

obj = SimpleJSONReader(res);

Where obj is an array of objects containing the misspelled word. obj[i].Word and lists of options for its replacement in the form of an array of strings obj[i].Suggestions. Another property obj[i].Replacement – this is the main option for replacing a word with an error.

Below is the full processing code:

SpellCheck.epf
&НаКлиенте
Перем ОшибкаОрфографии;


&НаКлиенте
Процедура ПроверитьОпечатки(Команда)
	ОшибкаОрфографии = Ложь;
	ПроверкаОрфографии();
КонецПроцедуры

&НаКлиенте
Процедура ПроверкаОрфографии()

	txt = СтрокаВвода;
	srv = Новый COMОбъект("SpellChecker.ComService");
	res = srv.SpellCheck(txt);

	Если СтрДлина(res) = 2 Тогда
		Если ОшибкаОрфографии Тогда
			Оповещение = Новый ОписаниеОповещения("ПродолжитьОбработкуТекста", ЭтотОбъект);
			ПоказатьЗначение(Оповещение,
				"Отлично!" + Символы.ПС + "  Все ошибки исправлены!");
			ОшибкаОрфографии = Ложь;
		Иначе
			ПродолжитьОбработкуТекста();
		КонецЕсли
	Иначе
		ОшибкаОрфографии = Истина;
		obj = ПростоеЧтениеJSON(res);
		wor = obj[0].Word;
		rep = obj[0].Replacement;
		Оповещение = Новый ОписаниеОповещения("ПослеЗакрытияВопроса", ЭтотОбъект, obj[0]);
		ПоказатьВопрос(Оповещение, "Заменить " + wor + " на " + rep + "?",
								РежимДиалогаВопрос.ДаНет,,, "Ошибка орфографии");
	КонецЕсли

КонецПроцедуры

&НаКлиенте
Процедура ПослеЗакрытияВопроса(Результат, Параметры) Экспорт

    Если Результат = КодВозвратаДиалога.Да Тогда
		txt = СтрокаВвода;
		wor = Параметры.Word;
		rep = Параметры.Replacement;

		СтрокаВвода = СтрЗаменить(txt, wor, rep);

		ПроверкаОрфографии();
	Иначе
		ПродолжитьОбработкуТекста();
    КонецЕсли;

КонецПроцедуры

Функция ПростоеЧтениеJSON(Данные)

	ЧтениеJSON = Новый ЧтениеJSON;
	ЧтениеJSON.УстановитьСтроку(Данные);
	Возврат ПрочитатьJSON(ЧтениеJSON);

КонецФункции

&НаКлиенте
Процедура ПродолжитьОбработкуТекста(Параметры = Неопределено) Экспорт
	// Что то делаем дальше
КонецПроцедуры

For example, I made a mold for processing like this, without any frills, you can now modify it as it will be convenient for you for your task

And now, if you correctly name all the elements on the form, then when you click the “Check typos” button, the processing code given above will sequentially correct all the erroneous words in the input field.

Conclusion

I understand that COM technology is probably outdated, everyone has already switched to web services, REST API, gRPC, Message Brokers, WebSockets and GraphQL, but I decided to try to integrate C# into 1C directly, and since it was not possible to insert a C# dll into 1C without COM, I wrapped it in COM. And by the way, no matter how I tried to add the OnComplete event to the SpellChecker COM object, nothing worked. If anyone is interested, here is the code

using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace SpellCheckerV2;


[ComVisible(true)]
[Guid("fe103d6e-e71b-414c-80bf-982f18f23235"),
    InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IComService
{
    string SpellCheck(string text);
}

[ComVisible(true)]
[Guid("fe103d6e-e71b-414c-80bf-982f18f23236"),
    InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ISpellCheckerEvents
{
    [DispId(1)]
    public void OnComplete(string msg);
}

[ComVisible(true)]
[Guid("fe103d6e-e71b-414c-80bf-982f18f23238")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ISpellCheckerEvents))]
public class ComService : IComService
{
    [ComVisible(true)]
    public Action<string>? OnComplete;

    public string SpellCheck(string text)
    {
        var spellCheckResults = SpellCheckerAPI.SpellCheck(text);
        string x = JsonSerializer.Serialize(spellCheckResults);
        string jsonString = Regex.Unescape(x);

        OnComplete?.Invoke("Ok");
        return jsonString;
    }
}

This code does not work, does not publish, as expected, the OnComplete event, 1C does not see it. And accordingly, if you write in 1C:

srv = New COMObject(“SpellChecker.ComService”);
AddHandler srv.OnComplete, Pikaka;

in the line where AddHandler tries to find the srv.OnComplete event, an error occurs.

Well, as they say, a negative result is also a result.

Bye friends, I hope for a tough shit.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *