Manage Notes / My Notifications in RTC using a COM Stream Wrapper

Manage Notes / My Notifications in RTC using a COM Stream Wrapper

  • Comments 2

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

  1. Open Visual Studio (in this example I am using Visual Studio 2010) with elevated privileges (Run As Administrator)
  2. 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

  1. Go to Project > Properties (StreamWrp Properties...)
  2. From the Project Properties form go to "Application*" tab
  3. Enable on Resources, the "Icon and Manifest" option
  4. Click on "Assembly Information" button
  5. In the "Assembly Information" form use the following GUID (or create brand new one):
    • 74f87d09-198a-4d81-a056-53271f21d4dd
  6. In the "Assembly Information" form check "Make assembly COM-Visible" and click OK
  7. From the Project Properties form go to the "Build" tab
  8. In the "General" section tick "Define DEBUG content", "Define TRACE content" and "Allow unsafe code"
  9. From the Project Properties form go to the "Signing" tab
  10. Tick "Sign the assembly"
  11. Create a new Strong Name Key (SNK) e.g. SWtest.snk

C. Develop (add code to) your StreamWrp project

  1. Add References (replace the existing ones) to the following Namespaces
    • System
    • System.Data
    • System.XML
  2. Locate your Class1.cs file in the Solution Explorer
  3. Right click > View Designer (Shift+F7)
  4. 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

  1. 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).
  2. 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

  1. 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)
  2. Launch the Visual Studio Command Prompt (VSCP) with elevated privilege (Run As Administrator).
  3. 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)
  4. 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

Attachment: StreamWrpCU.txt
Leave a Comment
  • Please add 8 and 2 and type the answer here:
  • Post
  • Thank you very much for your post Duilio,

    Do you have any Ideas to write this code in a codeunit NAV 2009 R2 ? it's more easier to deploy it ?

    Thank you.

  • Hi Mehdi. If I may resume what you requested, I guess you refer the fact of using .NET interop instead of developing a Wrapper, right? Actually I have not tried that and I cannot say if this can be easily done or there are any limitations about it (I have in mind the behavior of the stream with .NET interop).

    Surely this will result in a more flexible usage e.g. if you need to make any modification, now, you need to recompile and redispatch the wrapper dll on each client while in the way you underline you just have to modify C/AL (better... .NET interop) code inside the codeunit and recompile it.

    Let me see if I have room to try this out. No guarantee.

Page 1 of 1 (2 items)