With NAV 2009 it has been made available to the users a messaging system based on Notes / MyNotes page parts. Note records are stored as BLOBs inside table 2000000068 Record Link. It is known that you cannot handle Notes using normal C/AL code and, in particular, correctly stream in and out the content of those BLOB fields.
![]()
In this blog you will find the source code in order to implement a COM Stream Wrapper object to write and read Notes. You may use this COM object to generate and handle Notes when needed without being bound to Notes and MyNotes system part. This object may give you much more flexibility in your RTC code development.
NOTE: the Stream Wrapper is working correctly (writing) only when code is executed in a RTC based environment. It will give unpredictable and wrong results if executed using Classic Client.
If you want to know more about Notes you can refer to MSDN link:
Touring the RoleTailored Client Pages http://msdn.microsoft.com/en-us/library/dd301400.aspx
My ingredients:
- NAV 2009 SP1 (with latest HF applied)
- Visual Studio 2010 Professional
- VS Command Prompt 2010 (from SDK)
- Windows 7 Enterprise
Develop the COM StreamWrapper.dll in Visual Studio
A. Create a New Class Project
- Open Visual Studio (in this example I am using Visual Studio 2010) with elevated privileges (Run As Administrator)
- Create a New Project (CTRL+SHIFT+N) with these parameters
- Visual C# - Windows
- Class library
- .NET Framework 3.5
- Name: StreamWrp
- Location: C:\TMP (or whatever location you like)
- Solution Name: StreamWrp
- Create directory for solution
B. Create a Strong Name Key (SNK) and set correct Properties for the Class project
- Go to Project > Properties (StreamWrp Properties...)
- From the Project Properties form go to "Application*" tab
- Enable on Resources, the "Icon and Manifest" option
- Click on "Assembly Information" button
- In the "Assembly Information" form use the following GUID (or create brand new one):
- 74f87d09-198a-4d81-a056-53271f21d4dd
- In the "Assembly Information" form check "Make assembly COM-Visible" and click OK
- From the Project Properties form go to the "Build" tab
- In the "General" section tick "Define DEBUG content", "Define TRACE content" and "Allow unsafe code"
- From the Project Properties form go to the "Signing" tab
- Tick "Sign the assembly"
- Create a new Strong Name Key (SNK) e.g. SWtest.snk
C. Develop (add code to) your StreamWrp project
- Add References (replace the existing ones) to the following Namespaces
- System
- System.Data
- System.XML
![]()
- Locate your Class1.cs file in the Solution Explorer
- Right click > View Designer (Shift+F7)
- Replace the C# code automatically written with the one written below
// Copyright © Microsoft Corporation. All Rights Reserved.
// This code released under the terms of the
// Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
namespace StreamWrp
{
[ComVisible(true)]
[Guid("2F870D88-FEA5-4F27-81FB-6775D7436E52")]
public interface IStreamHelper
{
string Text
{
get;
set;
}
int Transform(int encodeRead, int encodeWrite, IStream reader, IStream writter);
}
[ClassInterface(ClassInterfaceType.None)]
[Guid("B4E5F8F4-5225-4B3A-998A-B82A8A7C6B8E")]
public class StreamHelper : IStreamHelper
{
public string Text
{
get
{
throw new Exception("The method or operation is not implemented.");
}
set
{
throw new Exception("The method or operation is not implemented.");
}
}
public int Transform(int encodeRead, int encodeWrite, IStream reader, IStream writter)
{
byte[] pv = new byte[4098];
int read = 0;
unsafe
{
IntPtr pcbRead = new IntPtr(&read);
reader.Read(pv, pv.Length, pcbRead);
}
MemoryStream innerStream = new MemoryStream(pv, 0, read);
string note = String.Empty;
if (innerStream.Length != 0)
{
//Select InS encoding and ReadChars Ins
Encoding inEncode;
if (encodeRead != 0)
{
inEncode = Encoding.GetEncoding(encodeRead);
using (BinaryReader innerreader = new BinaryReader(innerStream, inEncode))
{
note = new string(innerreader.ReadChars((int)innerStream.Length));
innerreader.Close();
}
}
else
{
using (BinaryReader innerreader = new BinaryReader(innerStream))
{
note = new string(innerreader.ReadChars((int)innerStream.Length));
innerreader.Close();
}
}
MemoryStream stream2 = new MemoryStream();
//Select OutS Encoding and Write OutS
Encoding outEncode;
if (encodeWrite != 0)
{
outEncode = Encoding.GetEncoding(encodeWrite);
using (BinaryWriter writer = new BinaryWriter(stream2, outEncode))
{
writer.Write((string)note);
writer.Close();
pv = stream2.ToArray();
}
}
else
{
using (BinaryWriter writer = new BinaryWriter(stream2))
{
writer.Write((string)note);
writer.Close();
pv = stream2.ToArray();
}
}
unsafe
{
IntPtr pcbWrite = new IntPtr(&read);
writter.Write(pv, pv.Length, pcbWrite);
}
}
return read;
}
}
}
D. Strong sign and build your COM object
- Now it is all set up, you are ready to build your StreamWrapper COM object. In the main menu, go to "Build" > "Build StreamWrp F6" (this is automatically strong signed as per properties setting).
- NOTE : a WARNING message may arise
Warning 1 Type library exporter warning processing 'StreamWrp.IStreamHelper.Transform(reader), StreamWrp'. Warning: Type library exporter could not find the type library for 'System.Runtime.InteropServices.ComTypes.IStream'. IUnknown was substituted for the interface. StreamWrp
This is just a Warning about a substitution from IStream to IUnknown. There is no problem with this warning message. The compilation is successful.
E. Place DLLs into a folder and Register them
- Locate/Copypaste StreamWrp.dll and StreamWrp.tlb (should be in your \StreamWrp\StreamWrp\bin\Debug folder) in the Add-In folder of a machine where Role Tailored Client has been installed. (typically C:\Program Files\Microsoft Dynamics NAV\60\RoleTailored Client\Add-ins)
- Launch the Visual Studio Command Prompt (VSCP) with elevated privilege (Run As Administrator).
- In the VSCP, navigate through the location where you have located the dlls. (typically C:\Program Files\Microsoft Dynamics NAV\60\RoleTailored Client\Add-ins)
- Once you are located in the right directory type:
regasm /tlb:StreamWrp StreamWrp.dll /codebase
(hit return)
NOTE: there can be some warning messages
(type)
gacutil /I StreamWrp.dll
(hit return)
![]()
Develop the C/AL code
A. Develop the C/AL code to WRITE and READ Notes in RTC environments
How this COM Wrapper works? It accepts a Stream and returns a modified Stream. Nothing more.
It needs to be feed up with 2 encoding values depending if the Wrapper is used to write or read Notes.
A useful example of encoding (overall if there are special characters that need to be handled, e.g. double S, umlaut, etc.) can be found at this link:
http://msdn.microsoft.com/en-us/library/system.text.encoding.windowscodepage.aspx
In this example, I am using a DEU standard database, therefore I am using IBM437 CodePage to correctly encode/decode the stream (note that IBM437 is also part of the Windows CodePage 1252).
The 2 following Codeunits attached in TXT format are used to Write and Read Notes.
NOTE: in order to let this example works you must have, at least, 1 note created from RTC (it merely use a copy from the last record, just as example).
This is the C/AL code snippet to WRITE Notes using RTC
...
// Copyright © Microsoft Corporation. All Rights Reserved.
// This code released under the terms of the
// Microsoft Public License (MS-PL, http://opensource.org/licenses/ms-pl.html.)
IF ISSERVICETIER THEN BEGIN
CLEAR(NoteText);
// Add special chars
NoteText.ADDTEXT(STRSUBSTNO(Text1000000001,USERID,TODAY,TIME) + ' - ìèòàù - Österreich - ');
// Browse country table and create the Note by pasting Code and Name into the NoteText
CountryRec.RESET;
IF CountryRec.FINDFIRST THEN REPEAT
NoteText.ADDTEXT(' ** Country ' + FORMAT(CountryRec.Code)+ ' - ' + FORMAT(CountryRec.Name));
UNTIL CountryRec.NEXT = 0;
// Find the last Record Link to retrieve the ID
RecordLink.RESET;
RecordLink.FINDLAST;
LinkID := RecordLink."Link ID";
// Create ID+1 Record Link with empty Note (copy the link above)
LinkID := LinkID + 1;
RecordLink2.INIT;
RecordLink2."Link ID" := LinkID;
RecordLink2."Record ID" := RecordLink."Record ID";
RecordLink2.URL1 :=RecordLink.URL1;
RecordLink2.Type := RecordLink2.Type :: Note;
RecordLink2.Created := CURRENTDATETIME;
RecordLink2."User ID" := USERID;
RecordLink2.Company := COMPANYNAME;
RecordLink2.Notify := TRUE;
// Stream the NoteText inside the note
RecordLink2.CALCFIELDS(Note);
RecordLink2.Note.CREATEOUTSTREAM(OutS);
NoteText.WRITE(OutS);
RecordLink2."To User ID" := USERID;
RecordLink2.INSERT;
// Find the record inserted in order to 'adjust' it with the StreamWrapper
RecordLink2.INIT;
RecordLink2."Link ID" := LinkID;
RecordLink2.FIND('=');
RecordLink2.CALCFIELDS(Note);
RecordLink2.Note.CREATEINSTREAM(InS);
RecordLink2.Note.CREATEOUTSTREAM(OutS);
// Let the COM StreamWrapper transform the Blob correctly
EncodeIn := 437; //CodePage IBM437
EncodeOut := 0; //No CodePage in output
IF ISCLEAR(Transform) THEN
CREATE(Transform);
InSVar := InS;
OutSVar := OutS;
Transform.Transform(EncodeIn, EncodeOut, InSVar, OutSVar);
RecordLink2.MODIFY();
END;
MESSAGE('WRITE : DONE');
...
And this is the C/AL code snippet to READ Notes using RTC
...
IF ISSERVICETIER THEN BEGIN
CLEAR(TempBlobRec);
CLEAR(NoteText);
// Find the right Record Link
RecordLink.RESET;
RecordLink.FINDLAST;
IF RecordLink.Note.HASVALUE THEN BEGIN
RecordLink.CALCFIELDS(Note);
RecordLink.Note.CREATEINSTREAM(InS); //Note --> InS
// Init a Temp Blob
IF TempBlobRec.GET(10000) THEN
TempBlobRec.DELETE;
TempBlobRec.INIT;
TempBlobRec."Primay Key" := 10000;
TempBlobRec.INSERT;
// Stream the 'modified back' Note onto this Blob field
TempBlobRec.GET(10000);
TempBlobRec.CALCFIELDS(Blob);
TempBlobRec.Blob.CREATEOUTSTREAM(OutS);
// Let the COM StreamWrapper transform the Blob correctly
EncodeIn := 0; // Read raw data from BLOB
EncodeOut := 437; // Use CodePage IBM437 in output
IF ISCLEAR(Transform) THEN
CREATE(Transform);
InSVar := InS;
OutSVar := OutS;
Transform.Transform(EncodeIn, EncodeOut, InSVar, OutSVar);
TempBlobRec.MODIFY;
// Get the modified Blob rec and read it
TempBlobRec.GET(10000);
TempBlobRec.CALCFIELDS(Blob);
TempBlobRec.Blob.CREATEINSTREAM(InS2);
// Algorithm to READ the Blob output
IsFirstTxtLine := TRUE;
WHILE NOT (InS2.EOS()) DO BEGIN
Int:= InS2.READ(Txt);
IF Int <> 0 THEN BEGIN
IF IsFirstTxtLine THEN BEGIN
LengthStr := STRLEN(Txt);
CASE LengthStr OF
1..126 : Txt := COPYSTR(Txt,3,STRLEN(Txt));
127 : Txt := COPYSTR(Txt,4,STRLEN(Txt));
ELSE
Txt := COPYSTR(Txt,5,STRLEN(Txt));
END;
IsFirstTxtLine := FALSE;
END;
MESSAGE(Txt);
END;
CLEAR(Txt);
CLEAR(Int);
END;
END;
END;
MESSAGE('READ - DONE');
...
These postings are provided "AS IS" with no warranties and confer no rights. You assume all risk for your use.
Duilio Tacconi (dtacconi)
Microsoft Dynamics Italy
Microsoft Customer Service and Support (CSS) EMEA
A special thanks to Jorge Alberto Torres - DK-MBS NAV Development