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 файл, и просматриваю содержимое в моей любимой программе для просмотра 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? – давайте попробуем.
Я добавил указанный ниже код в Кодеюнит и запустил его в Роле-ориентированном и Классическом Клиенте.
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-объекте, которую можно было бы назвать:
Далее AL бизнес логика заключается в вызове и соответствующей проверке — без большого количества разбора XML и прочего.
Этот вариант более предпочтительный, но предполагает некоторый рефакторинг и использование нового СОМ-объекта, который должен быть установлен на сервере.
Использование временного файла, чтобы передать данные
Как уже упоминалось ранее, в NAV 2009, при создании файла с бинарными данными, создается файл в OEM –формате, который будет иметь одинаковые двоичные данные, которые мы набрали в AL-редакторе.
Таким образом, используя временный файл, Вы можете создать строку, которую вы хотите отправить XMLHTTP, создать новый объект COM, который будет содержать функцию, которая посылает содержимое файла объекту XMLHTTP и записать полученный ответ обратно в тот же файл для последующего чтения в NAV.
Идея заключается в том, что файлы (и byte[]) являются двоичными данными – строки, в свою очередь, ими не являются.
Функция в COM-объекте может выглядеть следующим образом:
{
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 код изменится с:
(следует открыть ResponseStream) на:
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-объекте Вы можете создать функцию, которая будет возвращает символ на основе его числового представления:
{
return ""+(char)ch;
}
Проблема этого направления заключается в том, что эта функция должна использоваться только при работе с Роле-Ориентированным клиентом.
Выполняя вызов этой функции в Классическом Клиенте, мы получим Unicode-строку, которая снова будет преобразована в OEM-формат – поэтому эти манипуляции вообще не будут иметь смысла.
Преобразование из/в Unicode, используя строки вместо файлов
А, что если данные, которые Вам необходимо отправить, является конфиденциальным и Вы не можете записать их в файл?
Ну — вы можете создать функцию, которая преобразует строку к OEM — отправить ее по сети, а затем преобразовывать ответ в Unicode (таким образом, когда строка будет получена в NAV – она снова будет преобразована в OEM).
Кажется, что происходит очень большое количество операция перекодирования туда и обратно, но, тем не менее, это реально работает как для Классического, так и для Роли-ориентированного Клиента. Код, реализующий данный функционал указан ниже:
{
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. Дополнительная информация об авторе появится вскоре.
Недавно писал ф-ю конвертации из «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);
нужна была перекодировка именно русских букв, со всякими греческими и прочими кодировками не заморачивался.
написать обратный кодер, думаю, тоже особых проблем не составит.
Как-то криво в предыдущем посте вставилось.
Так правильнее:
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);
и снова символы «проглотились»
//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);
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);
Во, так лучше.
Прошу прощения за спам, но первые несколько попыток почему-то «выгрызали» куски из кода. Последняя попытка удачна, но нужно удалить символы «***»
хотя нет. Снова код погрызен — так что пример не рабочий.