NAV 2009 и Юникод!

Название может немного ввести в заблуждение, но я начал писать этот пост, как ответ на проблему, с которой столкнулся мой партнер, работая с NAV 2009 – проблема была связана с кодировкой Unicode. Я не эксперт по Unicode, поэтому не обижайтесь на меня, если я назову некоторые вещи неправильно.

Как вы знаете, NAV 2009 состоит из трёхуровневой архитектуры и уровня служб и на 95% состоят из управляемого кода (лишь небольшая часть стека данных, по-прежнему остается неуправляемой). Вы, наверное, также знаете, что управляемый код изначально поддерживает Unicode — на самом деле строки в C# или VB.Net по умолчанию являются Unicode-строками.

В C# вы используете тип Byte[], если вам нужно работать с двоичными данными. В моём предыдущем посте о передаче двоичных данных между Уровнем Службы и COM на стороне клиента или потребителя веб-службы (вы можете найти его здесь), я прочитал содержимое файла в Byte[] и использовал base64 – кодирование, чтобы закодировать его содержимое в строку, которая потом может передаваться между платформами, кодом страниц и т.д.

Совместим ли NAV 2009 с Unicode?

Нет, мы не можем это утверждать. Есть много вещей, которые мешают нам пользоваться продуктом полностью совместимый с Unicode — но мы движемся в этом направлении. Некоторые из таких вещей:

  • среда разработки и Классический Клиент не поддерживают Unicode. Когда вы пишете строки AL- кода они находятся в OEM.
  • Импорт/экспорт выполняется в OEM-формате.
  • На самом деле я не знаю, какой формат используется внутри SQL, но я полагаю, что это не Unicode (опять же потому, что мы имеем дело со смешенными платформами).

Давайте рассмотрим пример:

В кодеюните я пишу следующую строку:

txt := 'ÆØÅ are three Danish letters, ß is used in German and Greek.';

Теперь, я экспортирую это в .txt файл, и просматриваю содержимое в моей любимой программе для просмотра DOS-текста:

Как вы можете заметить — символ Ø изменился, и при беглом взгляде на кодировку страницы, становится понятно, что используется 437 кодировка, которая не содержит символа Ø.

Если открыть экспортированный файлы в Блокноте, то он тоже будет выглядеть по-другому:

Txt := ‘’î are three Danish letters, á is used in German and Greek.’;

Основная причина данной ситуации в том, что Блокнот допускает использование Unicode, а .txt файл в OEM-кодировке. Используя Роле-ориентированный клиент, давайте, при помощи Блокнота, посмотрим на строку, которая была сгенерирована C# в исходный файл:

txt = new NavText(1024, @»ÆØÅ are three Danish letters, ß is used in German and Greek.»);

Хорошо – мы знает, что Блокнот совместим с Unicode и C# использует Unicode-строки, таким образом, AL -> C# компилятор конвертирует строку в Unicode – и вот как это выглядит в моей программе для просмотра DOS – текста:

Ясно, что мои строки были закодированы.

Но подождите…

Если NAV 2009 преобразовывает мои текстовые константы в Unicode — что, если я запишу эту строку в файл, используя, хорошо известные, команды для работы с файлами в AL? – давайте попробуем.

Я добавил указанный ниже код в Кодеюнит и запустил его в Роле-ориентированном и Классическом Клиенте.

file.CREATETEMPFILE();
filename := file.NAME;
file.CLOSE();
file.CREATE(filename);
file.CREATEOUTSTREAM(OutStr);
OutStr.WRITETEXT(Txt);
file.CLOSE();

и не волнуйтесь, результат одинаковый – обе платформы фактически пишут файл в формате OEM (мы возможно предпочли, чтобы это был Unicode, но из соображений совместимости это, вероятно, хорошая вещь).

Другая вещь, которую мы должны попробовать, состоит в том, чтобы вызвать управляемый COM-объект и посмотреть на строку, которую мы получим — и снова мы получим одинаковые строки используя Классический или Роле-Ориентированный Клиент, но в данном случае, мы НЕ получаем строку в формате OEM — мы получаем строку в Unicode кодировке.

MSXML

Теперь, если мы вызываем неуправляемый COM-объект (например, msxml2 — XMLHTTP), фактически мы получаем OEM-строку при работе с Классическим клиентом и Unicode-строку в Роле-Ориентированном Клиенте. Обычно XMLHTTP используется только с ASCII — но в некоторых случаях, может использоваться и с двоичными данным – в котором данные кодируются в диапазоне от 128-255 символов.

На текущий момент наша задача состоит в том, чтобы двоичные данные (которые не имеют никакого отношения к любым специальным символам) преобразовать в Unicode — и поставщик веб-службы не имел шансов «угадать», что мы подразумеваем под данными, которые отправляем.

Следующей проблемой является то, что Роле-ориентированный Клиент не поддерживает тип Byte[] (двоичный) — в котором мы могли бы создавать и отправлять команды. Я перепробовал несколько вариантов, но не нашел способа отправить хоть какие-либо двоичные данные (более 128) для отправки команд XMLHTTP.

Третья проблема, связанная с XMLHTTP, заключается в том, что единственный способ получить ответ, это прочитать данные из ResponseText – информация в котором возвращается в формате Unicode и которая будет сконвертирована, прежде чем попадет в NAV.

Помните, что эти проблемы не будут возникать, в случае если провайдер веб-сервиса использует XML для передачи данных туда и обратно – так как XML ASCII-совместим.

Мое первое предложение, если у вас возникли проблемы с поставщиком веб-сервиса, используя двоичный процесс передачи данных отправлять и получать запросы в виде XML. Если это невозможно — у вас есть несколько вариантов (которые включают написание нового объекта COM).

Создание Прокси

Вы можете создать прокси веб-службу в качестве COM-компоненты (вероятно, на стороне сервера) и тем самым получить высокоуровневую функцию, которую Вы будете вызывать. Это позволит удалить код связующего звена с XMLHTTP из NAV и возложить всю работу на COM-объект.

Например — у вас есть поставщик веб-службы, с помощью которой можно проверять номера кредитных карт и обычно вы должны создать строку в AL и отправить ее с помощью команду Send — и затем проанализировать полученный ResponseStream, чтобы выяснить, все ли нормально или нет.

Создаем функцию в новом COM-объекте, которую можно было бы назвать:

int CheckCreditCard(string CreditCardNo, string NameOnCard, int ExpireMonth, int ExpireYear, int SecurityCode)

Далее AL бизнес логика заключается в вызове и соответствующей проверке — без большого количества разбора XML и прочего.

Этот вариант более предпочтительный, но предполагает некоторый рефакторинг и использование нового СОМ-объекта, который должен быть установлен на сервере.

Использование временного файла, чтобы передать данные

Как уже упоминалось ранее, в NAV 2009, при создании файла с бинарными данными, создается файл в OEM –формате, который будет иметь одинаковые двоичные данные, которые мы набрали в AL-редакторе.

Таким образом, используя временный файл, Вы можете создать строку, которую вы хотите отправить XMLHTTP, создать новый объект COM, который будет содержать функцию, которая посылает содержимое файла объекту XMLHTTP и записать полученный ответ обратно в тот же файл для последующего чтения в NAV.

Идея заключается в том, что файлы (и byte[]) являются двоичными данными – строки, в свою очередь, ими не являются.

Функция в COM-объекте может выглядеть следующим образом:

public int SendFileContent(object NAVxmlhttp, string filename)
{
    MSXML2.ServerXMLHTTP xmlHttp = NAVxmlhttp as MSXML2.ServerXMLHTTP;
    FileStream fs = File.OpenRead(filename);
    BinaryReader br = new BinaryReader(fs);

    int len = (int)fs.Length;
    byte[] buffer = new byte[len];
    br.Read(buffer, 0, len);
    br.Close();
    fs.Close();
    xmlHttp.send(buffer);

    buffer = xmlHttp.responseBody as byte[];
    fs = File.Create(filename);
    BinaryWriter bw = new BinaryWriter(fs);
    bw.Write(buffer);
    bw.Close();
    fs.Close();

    return xmlHttp.status;
}

Если вы будете использовать этот подход, то AL код изменится с:

XmlHttp.Send(Txt);

(следует открыть ResponseStream) на:

file.CREATETEMPFILE();
filename := file.NAME;
file.CLOSE();
file.CREATE(filename);
file.CREATEOUTSTREAM(OutStr);
OutStr.WRITETEXT(Txt);
file.CLOSE();
myCOMobject.SendFileContent(XmlHttp, filename);

(следует снова открыть файл — чтение результата).

Второй подход также будет работать и для Классического Клиента – таким образом нет необходимости использовать конструкцию IF ISSERVICETIER THEN.

В Edit In Excel — Part 4, можно увидеть, как создать COM-объект – здесь не должно возникнуть каких-либо трудностей.

Создайте двоичную строку, которая не будет закодирована

You could create a function in a COM object, which returns a character based on a numeric value:

В COM-объекте Вы можете создать функцию, которая будет возвращает символ на основе его числового представления:

public string GetChar(int ch)
{
    return ""+(char)ch;
}

Проблема этого направления заключается в том, что эта функция должна использоваться только при работе с Роле-Ориентированным клиентом.

Выполняя вызов этой функции в Классическом Клиенте, мы получим Unicode-строку, которая снова будет преобразована в OEM-формат – поэтому эти манипуляции вообще не будут иметь смысла.

Преобразование из/в Unicode, используя строки вместо файлов

А, что если данные, которые Вам необходимо отправить, является конфиденциальным и Вы не можете записать их в файл?

Ну — вы можете создать функцию, которая преобразует строку к OEM — отправить ее по сети, а затем преобразовывать ответ в Unicode (таким образом, когда строка будет получена в NAV – она снова будет преобразована в OEM).

Кажется, что происходит очень большое количество операция перекодирования туда и обратно, но, тем не менее, это реально работает как для Классического, так и для Роли-ориентированного Клиента. Код, реализующий данный функционал указан ниже:

public class MyCOMobject : IMyCOMobject
{
    private static Byte[] oem2AnsiTable;
    private static Byte[] ansi2OemTable;
    /// <summary>
    /// Initialize COM object
    /// </summary>
    public MyCOMobject()
    {
        oem2AnsiTable = new Byte[256];
        ansi2OemTable = new Byte[256];
        for (Int32 i = 0; i < 256; i++)
        {
            oem2AnsiTable[i] = (Byte)i;
            ansi2OemTable[i] = (Byte)i;
        }
        NativeMethods.OemToCharBuff(oem2AnsiTable, oem2AnsiTable, oem2AnsiTable.Length);
        NativeMethods.CharToOemBuff(ansi2OemTable, ansi2OemTable, ansi2OemTable.Length);
        // Remove "holes" in the convertion structure
        Int32 ch1 = 255;
        Int32 ch2 = 255;
        for (;; ch1--, ch2--)
        {
            while (ansi2OemTable[oem2AnsiTable[ch1]] == ch1)
            {
                if (ch1 == 0)
                    break;
                else
                    ch1--;
            }
            while (oem2AnsiTable[ansi2OemTable[ch2]] == ch2)
            {
                if (ch2 == 0)
                    break;
                else
                    ch2--;
            }
            if (ch1 == 0)
                break;
            oem2AnsiTable[ch1] = (Byte)ch2;
            ansi2OemTable[ch2] = (Byte)ch1;
        }
    }
    /// <summary>
    /// Convert Unicode string to OEM string
    /// </summary>
    /// <param name="str">Unicode string</param>
    /// <returns>OEM string</returns>
    private byte[] UnicodeToOem(string str)
    {
        Byte[] buffer = Encoding.Default.GetBytes(str);
        for (Int32 i = 0; i < buffer.Length; i++)
        {
            buffer[i] = ansi2OemTable[buffer[i]];
        }
        return buffer;
    }
    /// <summary>
    /// Convert OEM string to Unicode string
    /// </summary>
    /// <param name="oem">OEM string</param>
    /// <returns>Unicode string</returns>
    private string OemToUnicode(byte[] oem)
    {
        for (Int32 i = 0; i < oem.Length; i++)
        {
            oem[i] = oem2AnsiTable[oem[i]];
        }
        return Encoding.Default.GetString(oem);
    }
    /// <summary>
    /// Send data through XMLHTTP
    /// </summary>
    /// <param name="NAVxmlhttp">XmlHttp object</param>
    /// <param name="data">string containing data (in Unicode)</param>
    /// <returns>The response from the XMLHTTP Send</returns>
    public string Send(object NAVxmlhttp, string data)
    {
        MSXML2.ServerXMLHTTP xmlHttp = NAVxmlhttp as MSXML2.ServerXMLHTTP;
        byte[] oem = UnicodeToOem(data);
        xmlHttp.send(oem);
        return OemToUnicode((byte[])xmlHttp.responseBody);
    }
}
internal static partial class NativeMethods
{
    #region Windows OemToChar/CharToOem imports
    [DllImport("user32", EntryPoint = "OemToCharBuffA")]
    internal static extern Int32 OemToCharBuff(Byte[] source, Byte[] dest, Int32 bytesize);
    [DllImport("user32", EntryPoint = "CharToOemBuffA")]
    internal static extern Int32 CharToOemBuff(Byte[] source, Byte[] dest, Int32 bytesize);
    #endregion
}

К сожалению, я не нашел способ сделать это без использования COM-объектов.

Оригинал статьи доступен здесь: NAV 2009 and Unicode!

Автор:

Количество статей, опубликованных автором: 3. Дополнительная информация об авторе появится вскоре.

Комментарии (6 комментариев)

  1. Георгий

    Недавно писал ф-ю конвертации из «nav» в unicode (в utf8):

    win2utf8(winText: text1024) : Text1024 // название ф-ии, набрал здесь вручную, остальное копипаст

    utf8Text := »;
    FOR pos:=1 TO STRLEN(winText) DO BEGIN
    oneChar := winText[pos];
    strPart := »;
    IF (oneChar=255) THEN oneChar := 32;
    IF ((oneChar>=’А’)AND(oneChar=’р’)AND(oneChar<='я')) THEN BEGIN // р..я
    prefixChar := 209;
    tmpChar := 'р';
    suffixChar := 128+(oneChar-tmpChar);
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    END
    ELSE BEGIN
    strPart := FORMAT(oneChar);
    END;
    END;
    IF (oneChar=184) THEN BEGIN // ё
    prefixChar := 209;
    suffixChar := 145;
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    END;
    IF (oneChar=184) THEN BEGIN // Ё
    prefixChar := 208;
    suffixChar := 129;
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    END;
    IF ((STRLEN(utf8Text)+STRLEN(strPart))<=MAXSTRLEN(utf8Text)) THEN
    utf8Text:=utf8Text+strPart
    ELSE ERROR(utf8Error);
    END;
    EXIT(utf8Text);

    нужна была перекодировка именно русских букв, со всякими греческими и прочими кодировками не заморачивался.

    написать обратный кодер, думаю, тоже особых проблем не составит.

  2. Георгий

    Как-то криво в предыдущем посте вставилось.

    Так правильнее:

    utf8Text := »;
    FOR pos:=1 TO STRLEN(winText) DO BEGIN
    oneChar := winText[pos];
    strPart := »;
    IF (oneChar=255) THEN oneChar := 32;
    IF ((oneChar>=’А’)AND(oneChar=’р’)AND(oneChar<='я')) THEN BEGIN // р..я
    prefixChar := 209;
    tmpChar := 'р';
    suffixChar := 128+(oneChar-tmpChar);
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    END
    ELSE BEGIN
    strPart := FORMAT(oneChar);
    END;
    END;
    IF (oneChar=184) THEN BEGIN // ё
    prefixChar := 209;
    suffixChar := 145;
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    END;
    IF (oneChar=184) THEN BEGIN // Ё
    prefixChar := 208;
    suffixChar := 129;
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    END;
    IF ((STRLEN(utf8Text)+STRLEN(strPart))<=MAXSTRLEN(utf8Text)) THEN
    utf8Text:=utf8Text+strPart
    ELSE ERROR(utf8Error);
    END;
    EXIT(utf8Text);

  3. Георгий

    и снова символы «проглотились»

    //utf8Text := »;
    //FOR pos:=1 TO STRLEN(winText) DO BEGIN
    // oneChar := winText[pos];
    // strPart := »;
    // IF (oneChar=255) THEN oneChar := 32;
    // IF ((oneChar>=’А’)AND(oneChar=’р’)AND(oneChar<='я')) THEN BEGIN // р..я
    // prefixChar := 209;
    // tmpChar := 'р';
    // suffixChar := 128+(oneChar-tmpChar);
    // strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    // END
    // ELSE BEGIN
    // strPart := FORMAT(oneChar);
    // END;
    // END;
    // IF (oneChar=184) THEN BEGIN // ё
    // prefixChar := 209;
    // suffixChar := 145;
    // strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    // END;
    // IF (oneChar=184) THEN BEGIN // Ё
    // prefixChar := 208;
    // suffixChar := 129;
    // strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    // END;
    // IF ((STRLEN(utf8Text)+STRLEN(strPart))<=MAXSTRLEN(utf8Text)) THEN
    // utf8Text:=utf8Text+strPart
    // ELSE ERROR(utf8Error);
    //END;
    //EXIT(utf8Text);

  4. Георгий

    utf8Text := »;
    FOR pos:=1 TO STRLEN(winText) DO BEGIN
    oneChar := winText[pos];
    strPart := »;
    IF (oneChar=255) THEN oneChar := 32;
    I***F ((oneChar>=’А’)AN***D(oneChar=’р’)AND(oneChar<='я')) TH***EN BE***GIN // р..я
    prefixChar := 209;
    tmpChar := 'р';
    suffixChar := 128+(oneChar-tmpChar);
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    E***ND
    EL***SE BE***GIN
    strPart := FORMAT(oneChar);
    E***ND;
    E***ND;
    IF (oneChar=184) TH***EN BE***GIN // ё
    prefixChar := 209;
    suffixChar := 145;
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    E***ND;
    IF (oneChar=184) TH***EN BE***GIN // Ё
    prefixChar := 208;
    suffixChar := 129;
    strPart := FORMAT(prefixChar)+FORMAT(suffixChar);
    E***ND;
    IF ((STRLEN(utf8Text)+STRLEN(strPart))<=MAXSTRLEN(utf8Text)) THEN
    utf8Text:=utf8Text+strPart
    ELSE ERROR(utf8Error);
    END;
    EXIT(utf8Text);

  5. Георгий

    Во, так лучше.
    Прошу прощения за спам, но первые несколько попыток почему-то «выгрызали» куски из кода. Последняя попытка удачна, но нужно удалить символы «***»

Добавить комментарий