AL Extensions: Importing and Exporting Media Sets

One of the things that has changed when you are building V2 extensions for a cloud environment is that you cannot access most functions that work with physical files.

This presents a bit of a challenge when it comes to working with the media and media set field types, as the typical approach is to have an import and export function so that a user can get pictures in and out of the field.

An example of this is the Customer Picture fact box that’s on the Customer Card:

WorkingWithMediaFields1

As you can see, the import and export functions in C/Side leverage the FileManagement codeunit in order to transfer the picture image to and from a physical picture file. These functions are now blocked.

So…..we have got to take another approach. Enter streams.

Using the in and out stream types we can recreate the import and export functions without using any of the file based functions.

An import function would look like the following. In this example, the Picture field is defined as a Media Set field.

local procedure ImportPicture();
var
   PicInStream: InStream;
   FromFileName: Text;
   OverrideImageQst: Label 'The existing picture will be replaced. Do you want to continue?', Locked = false, MaxLength = 250;
begin
   if Picture.Count > 0 then
     if not Confirm(OverrideImageQst) then
       exit;

  if UploadIntoStream('Import', '', 'All Files (*.*)|*.*', FromFileName, PicInStream) then begin
    Clear(Picture);
    Picture.ImportStream(PicInStream, FromFileName);
    Modify(true);
  end;
end;

The UploadIntoStream function will prompt the user to choose a local picture file, and from there we upload that into an instream. At no point do we ever put the physical file on the server. Also note that the above example will always override any existing picture. You do not have to do this as media sets allow for multiple pictures. I’m just recreating the original example taken from the Customer Picture page.

For the export we have to write a bit more code. When using a Media Set field, we do not have access to any system function that allows us to export to a stream. To deal with this all we need to do is loop through the media set and get each of the corresponding media records. Once we have that then we can export each of those to a stream.

That would look like this:

local procedure ExportPicture();
var
   PicInStream: InStream;
   Index: Integer;
   TenantMedia: Record "Tenant Media";
   FileName: Text;
begin
   if Picture.Count = 0 then
      exit;

   for Index := 1 to Picture.Count do begin
      if TenantMedia.Get(Picture.Item(Index)) then begin
         TenantMedia.calcfields(Content);
         if TenantMedia.Content.HasValue then begin
            FileName := TableCaption + '_Image' + format(Index) + GetTenantMediaFileExtension(TenantMedia);
            TenantMedia.Content.CreateInStream(PicInstream);
            DownloadFromStream(PicInstream, '', '', '', FileName);
         end;
      end;
   end;
end;

We use the DownloadFromStream function to prompt the user to save each of the pictures in the media set. As in our first example, there are no physical files ever created on the server, so we’re cloud friendly!

You may notice that I use the function GetTenantMediaFileExtension in the export example to populate the extension of the picture file. Since the user can upload a variety of picture file types, we need to make sure we create the file using the correct format.

The function to do this is quite simple, however there is no current function in the product to handle it, so you’ll have to build this yourself for now. Hopefully in the near future this function will be added by Microsoft.

local procedure GetTenantMediaFileExtension(var TenantMedia: Record "Tenant Media"): Text;
begin
   case TenantMedia."Mime Type" of
      'image/jpeg' : exit('.jpg');
      'image/png' : exit('.png');
      'image/bmp' : exit('.bmp');
      'image/gif' : exit('.gif');
      'image/tiff' : exit('.tiff');
      'image/wmf' : exit('.wmf');
   end;
end;

Until next time, happy coding!

 

Advertisements

Learn AL: New string features

The AL language is bringing some nice features to Dynamics NAV developers. I want to cover a couple of the cool new string features.

  1. Function chaining
  2. Replace() function

The following example is something I’ve used in the past numerous times. It finds and replaces a substring within a string. Created in Visual Studio Code, but using good ol’ C/AL syntax, the code would look something like this:

NewString := DELSTR(OriginalString,STRPOS(OriginalString,FindString))
                + ReplaceWithString + COPYSTR(OriginalString,STRPOS(OriginalString,FindString) + STRLEN(FindString));

The above function is easy enough to create, but it was always something that I thought should have been part of the system functions. In AL, we now have the Replace() function, which can be used like this:

NewString := OriginalString.Replace(FindString,ReplaceWithString);
Oh by the way……invoking string functions directly from the variable!? Yes!
Using the “<variable>.<function>” syntax, we can now chain multiple functions together like this:
NewString := OriginalString.Replace(FindString,ReplaceWithString).ToLower();

Whoah! Pretty cool right!?

If you’re not always familiar with function chaining, I should point out that the functions in the chain get applied to the string from left to right, Depending on what you are doing to the string, the order you define the functions in the statement will matter.

This is some fantastic stuff and really brings Dynamics NAV development inline with current development practices.

Until next time, happy coding!

NAV Supporter’s Blog

Back in February, Microsoft resurrected one of their old NAV developer blogs. It’s been renamed to the NAV Supporter’s Blog and as Microsoft puts it:

“This will be by and for people who support the Dynamics NAV products in one way or the other.”

This blog is not necessarily about new front-end features in the product, but more about tips and tools that can be used to better support and diagnose the product.

Recent posts include:
  • Multi-part series on a new NAV diagnostics toolkit
  • NAV performance troubleshooting
  • Build overviews for NAV 2013 –> 2017

The blog can be found here. Check it out!

Happy coding!

Find Out if an Extension is Installed, part 2

“Hi, this is Library, is Extension 1 home? What about Extension 2, are they home too?”

In my previous post on this topic, I explained how you can use the NAV App Installed App system table to see if a specific extension is installed. I later found out though that the ability to access that table may not always be available in the Dynamics 365 for Financials platform, so back to square one. I want a stable long-lasting solution.

Alas…events!

First…some background on why I need this functionality. I’m developing 2 extensions that share a common library, but I want to develop these extensions so that they are independent, meaning that a customer is able to install only one of the extensions if they choose to, and not be forced to install both. I also need certain features in each extension to act differently depending on if the other extension is installed or not. I know….never simple. 🙂

For those that are not aware, when you submit an extension to AppSource, you are able to submit along with it a library extension. Multiple extensions can be dependent on the same library, which makes it easy to deliver foundation type functions that are shared amongst your extensions. The library extension will not be seen in AppSource, but it will be automatically installed when any of the dependent extensions are installed.

What this also means is that when I am developing in the context of one of my “functional” extensions, I am able to directly reference objects that are contained in my library because the library is guaranteed to exist when the functional extension is installed. What I cannot do though is the opposite, because although the library extension knows that at least one functional extension was installed, it does not know directly which one it was. Make sense!? Clear as mud I know. 🙂

In the example below, let’s assume that I have a “Library” extension, and two functional extensions, brilliantly named “Extension 1” and “Extension 2”.

First, I need to create a codeunit in my library extension that will act as the “extension handler” so to speak. In other words it will house the functions required to determine what extensions are installed. In my library codeunit, I’ll add the following functions:

CheckIsExtensionInstalled(ExtensionAppID : GUID) : Boolean
IsInstalled := FALSE;
IsExtensionInstalled(ExtensionAppID,IsInstalled);
EXIT(IsInstalled);

GetExtension1AppID() : GUID
EXIT('743ba26c-1f75-4a2b-9973-a0b77d2c77d3');

GetExtension2AppID() : GUID
EXIT('a618dfa7-3cec-463c-83f7-7d8f6f6d699b');

LOCAL [IntegrationEvent] IsExtensionInstalled(ExtensionAppID : GUID;VAR IsInstalled : Boolean)

The above functions allow me to call into the codeunit to determine if either of my functional extensions are installed. The GUIDs that I used are samples above, but you should use the same GUID that is used in the corresponding extension manifest file.

The piece that makes this all possible is the published event IsExtensionInstalled. What I can do now is from within each functional extension, I can subscribe to that event so that each extension can basically give “answer” the library when it asks if it’s installed.

To do that, I create 2 more codeunits, one in each functional extension. These codeunits will contain a subscriber to the event that we published in the library codeunit. This way, if the extension is installed, its subscriber will respond to the event and let the library know that it is installed. If the extension is not installed then there won’t be anyone home to answer that call.

Extension 1

LOCAL [EventSubscriber] OnCheckIsExtensionInstalled(ExtensionAppID : GUID;VAR IsInstalled : Boolean)
IF ExtensionAppID = ExtensionHandler.GetExtension1AppID THEN
  IsInstalled := TRUE;

Extension 2

LOCAL [EventSubscriber] OnCheckIsExtensionInstalled(ExtensionAppID : GUID;VAR IsInstalled : Boolean)
IF ExtensionAppID = ExtensionHandler.GetExtension2AppID THEN
  IsInstalled := TRUE;

So how do we use all of this? Easy, of course. The example below shows code that you could use from either of the functional extensions, so that your extensions can act differently depending on what other extensions are installed.

IF LibraryCodeunit.CheckIsExtensionInstalled(LibraryCodeunit.GetExtension1AppID) THEN
  MESSAGE('Extension 1 is installed.');

IF LibraryCodeunit.CheckIsExtensionInstalled(LibraryCodeunit.GetExtension2AppID) THEN
  MESSAGE('Extension 2 is installed.');

There, easy right? If you want to try it out yourself, you can grab the above example on GitHub here.

Now, while you can do this if you own all of the source code for each extension, you cannot use this solution to determine if “any old random extension” is installed, as you need to add the subscriber function to the functional extension code. But, if you are developing out multiple extensions and if you need to know which of them are installed, then this solution will work wonderfully!

Happy coding!

Timestamp fields in Dynamics NAV 2016

Have you ever needed to keep track of when records changed, or have you ever had to do an integration where you want to make sure you keep records in synch across multiple systems?

For master records, this was not a huge chore, as you typically were able to use the Last Date Modified field (if there is one of course), but of course you better hope that all updates to the record call the onModify() trigger. For transactional records though, this became more of a chore, as you would need to either keep track of the last entry you synchronized, or figure out some other way of keeping track what you’ve synched and what you haven’t.

I remember a particular integration that I had to do in which involved synchronizing the Item Ledger Entry table. Sure, I could keep track of the last entry no. so I could easily find all new records since the last synch session, however what became difficult was finding all of the older ledger entries that were updated as inventory transactions were applied to them. Remember that the Item Ledger Entry table is unique in that it gets updated even long after they get created.

Enter the Timestamp field. New to Dynamics NAV 2016, it will simply these types of integration and synchronization scenarios. As far as I can remember, all versions of Dynamics NAV running on SQL Server have had an internal system field name that holds the record timestamp. You could however, only access this field by querying the SQL table directly. Anything done via NAV would never even see the field.

Now with Dynamics NAV 2016, you have the ability for all records to determine which records have not only been inserted, but also modified. You still need to track your date/time reference (e.g. the last time the synch was run), and you are still left to determine what to do if records are deleted, but overall, this is a great new feature to the product!

Here’s how to use them…

  1. In any given table, create yourself a custom field, and assign it a Data Type of BigInteger. The name of the field can be anything you want.

  2. View the Properties of your new field, and set the SQL Timestamp property to Yes. It’s probably a good idea to make it a non-editable field as well, just for logic sake.

  3. Close, save, and compile your table.
  4. Voila, when you run the table, you now how your timestamp!

Umm, ok Mr. Blogger, I get all of this, but that field definitely does not contain either a date or a time!! What’s up with that??

Yes…this is true; the field does not actually hold date or time information. What it does hold though is a “version numbering” that is assigned to each row in a table, across all tables in the database, so every row in the database will have a unique timestamp value. This means that no matter what table you are dealing with, you can use a single reference timestamp and obtain all records that have been added or changed. AWESOME!
(as I said above though, finding deleted records is still a chore that requires coding)

One more note on Timestamp fields. You cannot add them to a table key, however you can still perform a SETCURRENTKEY using the timestamp, since NAV now allows us to sort on any field regardless of it being in a key or not.

So now you know, hopefully you can use this new feature to your advantage.

Happy coding….

MG