NAV 2009 R2: Mit dem DotNet-Datentyp die Tabelle “File” ersetzen

Mit dem Release von Dynamics NAV 2009 R2 vor zwei Tagen haben wir einen weiteren großen Schritt in Richtung NAV “7”, die .NET-Welt und die Integration in den Rest der Microsoft Produktpalette getan. Ich möchte mich auch nicht nochmals in all den neuen Features verlieren, sondern auf den neuen Datentyp “DotNet” fokussieren. Das im Zusammenhang mit der Tabelle File, die mir und anderen in der Vergangenheit regelmäßig Segen und Regen - oftmals auch zur gleichen Zeit - brachte.

Ich werde zukünftig des Öfteren ein wenig über neue und auch “alte wiederentdeckte” Features philosophieren. In diesem Fall versuche ich, auch .NET-Neulinge anzusprechen und zum Ausprobieren zu motivieren. Ich hoffe es gelingt mir, Kommentare sind erwünscht! Aber nun zur Sache:

Die Tabelle File

Zur Erinnerung: File ist eine virtuelle Tabelle, die zur Laufzeit den Zugriff auf Verzeichnisinhalte liefert. Um ein bestimmtes Verzeichnis nach Dateien oder anderen Verzeichnissen zu durchsuchen, filtert man auf den gewünschten Pfad und kann dann auf die enthaltenen Dateien oder Verzeichnisse über den Record zugreifen und beispielsweise per Schleife durchlaufen:

 File@42 : Record 2000000022;

File.SETRANGE(Path, 'C:\HeinzErhardt');
File.SETRANGE("Is a file", TRUE);
File.SETRANGE(Name, '*.txt');

IF File.FINDSET THEN REPEAT
  MESSAGE('Nochn Gedicht: ' + FORMAT(File.Name));
UNTIL File.NEXT = 0;

Oftmals ist einiges an Geschick und auch Fingerfertigkeit notwendig, um innerhalb einer Instanz des Record aktuelle Änderungen im gefilterten Pfad oder Verzeichniswechsel zeitnah abgebildet zu sehen. Dieses Verhalten ist der Art der Implementierung geschuldet, deshalb setze ich hier eine etwas andere Lösung um, die einfach in vorhandene Umgebungen übernommen werden kann, auf Basis des neuen NAV 2009 R2 DotNet Datentyps.

Microsoft .NET Framework

Die großartigste Entwicklungsplattform, das Microsoft .NET Framework, hält nun – ohne Umwege über COM-Wrapper und Automations - in Form des Datentyps “DotNet” Einzug in Dynamics NAV. Damit steht auch NAV-Entwicklern nun der direkte Zugang zu allen Möglichkeiten dieser Plattform offen. Dazu gehört nicht nur das .NET Framework allein, sondern auch alle weltweit verfügbaren Lösungen auf .NET-Basis und natürlich Ihre Eigenentwicklungen.
Der Kern des .NET Framework ist die Common Language Runtime (CLR), die Ausführungsschicht für .NET. Ganz einfach ausgedrückt werden in der CLR die Funktionen der Assemblies in einer gekapselten Umgebung ausgeführt. Wer mehr zu dem .NET Framework wissen möchte, dem empfehle ich die unendlichen Weiten unseres Microsoft Developer Network zu diesem Thema.

Assemblies und Namensräume

Verschiedene Funktionalitäten des .NET Framework sind in Assemblies zusammengefasst. Weiterhin ist .NET in sogenannte Namensräume (Namespaces) aufgeteilt, über die Bereiche wie Ein- und Ausgabe, Netzwerkzugriffe und Anderes voneinander getrennt sind. .NET Klassen, etwas das man im weitesten Sinne mit einer Dynamics NAV Codeunit vergleichen kann, kapseln Funktionen wie beispielsweise zur Ermittlung von Verzeichnisinhalten, um Dateien zu be- und verarbeiten oder auch um Informationen zu Dateien und Verzeichnissen selbst zu ermitteln. Alle genannten Funktionen finden sich im Namespace System.IO.

Zur Umsetzung benötige ich aus dem Namensraum System die Klasse Array und aus System.IO die Klassen DirectoryInfo und FileInfo. Alle genannten Klassen finden sich in der mscorlib.dll. Array nutze ich, um die von den Klassen DirectoryInfo und FileInfo zurückgegebenen Arrays mit Informationen zu Verzeichnissen und Dateien zu speichern. Diese Informationen werden dann in eine temporäre Instanz der Tabelle File geschrieben, in der Sie aus der NAV Businesslogik heraus dann wie bisher ansprechbar sind.

Die Umsetzung - Aufruf

Die zugrunde liegende Idee meines kleinen Projektes ist es, mit einfachen Mitteln eine vorhandene Nutzung der Tabelle File umstellen zu können. Dementsprechend war die Zielsetzung, maximal eine zusätzliche Variable anzulegen und auch nur eine weitere einfache Codezeile verwenden. Als Basis diente der obige Weg, auf ein Verzeichnis zu filtern, ggf. ein Namenspattern und eine Einschränkung auf Dateien anzugeben und dann mit dem Ergebnis zu arbeiten.
Um der Bedingung nur einer zusätzlichen Variablen Rechnung zu tragen, wird diese wohl eine Codeunit sein, in der ich die Funktionalität implementiere. Eine einfache Codezeile zusätzlich bedeutet, dass ich auch auf aufwendige Parametersetzungen verzichten muss. Die notwendigen Parameter können später aus dem gesetzten Filter ermittelt werden. Also ergibt sich in etwa folgendes Bild:

 TempFile@42 : TEMPORARY Record 2000000022;
FileHelper@43 : Codeunit 60000;

TempFile.SETRANGE(Path, 'C:\HeinzErhardt');
TempFile.SETRANGE("Is a file", TRUE);
TempFile.SETRANGE(Name, '*.txt');

FileHelper.FillFileTable(TempFile);

IF TempFile.FINDSET THEN REPEAT
  MESSAGE('Nochn Gedicht: ' + FORMAT(TempFile.Name));
UNTIL TempFile.NEXT = 0;

Wie Sie sehen können, hat sich nicht viel verändert. TempFile ist nun eine temporäre Variable, die ich des besseren Verständnisses wegen allerdings auch umbenannt habe. Ich empfehle auf jeden Fall diese Umbenennung, damit keine Verwirrungen über die Art der Variablen bei späteren Codereviews oder Änderungen entstehen. Weiterhin ist eine neue Variable (FileHelper) vom Typ Codeunit entstanden und ich habe eine zusätzliche Zeile implementiert: FileHelper.FillFileTable(TempFile) . Damit ist die Anforderung auf jeden Fall schon einmal erfüllt. Natürlich fehlt noch die Codeunit mit der Implementierung der Funktion. Aktuell also alles nur Show. Funktioniert aber die Umsetzung auf dieser Basis wie gedacht?

Die Umsetzung – Implementierung

Ich lege eine Codeunit 60000 “File Helper” an, erstelle eine neue Funktion FillFileTable() und speichere zunächst. Den benötigten Pfad erhalte ich über den gesetzten Filter. Weitere Filter müssen nicht ermittelt werden, da ich plane, alle gefundenen Einträge in die Tabelle zu übertragen und dann sorgen die eh gesetzten Filter dafür, dass nur die gewünschten Elemente ermittelt werden. Um den gesetzten Filter auf den Pfad nicht zu löschen, füge ich diesem noch einen Platzhalter hinzu, damit der Filter die gewünschten Einträge nicht ausfiltert.

 FillFileTable(VAR TempFile : TEMPORARY Record File) : Boolean

Path := TempFile.GETFILTER(Path);
TempFile.SETFILTER(Path, Path + '*');

Nun geht es ans Eingemachte: Ich habe ermittelt, dass die .NET Klasse DirectoryInfo mir die Methoden bietet, die ich benötige, um Verzeichnis- und Dateiinformationen zu erhalten. Beide Methoden liefern mir jeweils ein Array an Informationen zurück. Sammeln wir also zunächst die benötigten lokalen Variablen zusammen:

 DirectoryInfo@1010002 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.IO.DirectoryInfo";
Directories@1010005 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Array";
Files@1010006 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Array";
Directory@1010003 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.IO.DirectoryInfo";
File@1010004 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.IO.FileInfo";
Path@1010001 : Text[250];
i@1010007 : Integer;

Über die .NET Klasse DirectoryInfo ermittle ich später die Verzeichniseinträge. Die Arrays Directories und Files nehmen die Rückgabe der beiden Methoden auf, da diese Arrays vom Typ DirectoryInfo bzw. FileInfo returnieren. Directory und File stellen jeweils einzelne Elemente dieses Arrays dar. Weiterhin ist oben noch die Path Variable und eine Zählvariable i für die Iteration durch die Arrays definiert. Die Auswahl geschieht wie im Bild zu sehen, alle benötigten Klassen finden sich in der mscorlib.dll:

image

Doch wie bekommen wir nun was wir wollen? DirectoryInfo bietet die zwei Methoden GetDirectories() und GetFiles() . Da es sich dabei aber nicht um statische Methoden, sondern um Instanzmethoden handelt, muss zunächst eine Instanz dieser Klasse erzeugt werden. Für die  Instanziierung der Klasse wird der Konstruktor herangezogen. Eine Klasse kann mehrere Konstruktoren mit verschiedenen Parametern definiert haben. Das nennt man dann “Überladung”. Für DirectoryInfo ist das einfach, diese Klasse hat nur einen Konstruktor, der einen Parameter, nämlich den Pfad zum Basisverzeichnis erwartet. Dieses ist bereits ermittelt und steht in der Variable Path.

 DirectoryInfo := DirectoryInfo.DirectoryInfo(Path);

Mit dieser Zeile ist dann auch schon alles Notwendige geschehen. Wir haben nun eine Instanz der Klasse. Nun können die Instanzmethoden aufgerufen werden, mit denen wir unsere Arrays füllen wollen:

 Directories := DirectoryInfo.GetDirectories();
Files := DirectoryInfo.GetFiles();

Nun liegen in den Variablen Directories und Files jeweils schön aneinandergereiht alle Einträge die wir benötigen. Fehlt nur noch, diese in die übergebene Tabelle TempFile zu bekommen. Da aber die Integration von .NET gut gelungen ist, kann man direkt in einer Zählschleife die jeweiligen Werte auslesen und den zwei Klassen Directory und File zuweisen. Das ist notwendig, da wir darüber die eigentlichen Eigenschaften/Werte der zugrunde liegenden Klassen abrufen können:

 FOR i := 0 TO Directories.Length() - 1 DO BEGIN
  Directory := Directories.GetValue(i);
  AddFileEntry(TempFile, Directory.FullName(), Directory.Name(),
      FALSE, 0, Directory.LastWriteTime());
END;

FOR i := 0 TO Files.Length() - 1 DO BEGIN
  File := Files.GetValue(i);
  AddFileEntry(TempFile, File.FullName(), File.Name(),
      TRUE, File.Length(), File.LastWriteTime());
END;

Was passiert hier? Eine Schleife wie man sie täglich programmiert! Der Unterschied ist nur die Nutzung der .NET Klassen zwischendrin. Directories.Length() liefert die Anzahl der Elemente des Arrays Directories, das erste Element liegt auf Position 0, die Arraydimensionen beginnen bei DotNet-Datentypen, im Gegensatz zu C/AL, also bei 0. GetValue() liefert das Element (Directory bzw. File) auf der jeweiligen Position i. Die zwei Aufrufe der Prozedur AddFileEntry() , die ich weiter unten noch darstelle, zeigen, dass die Eigenschaften/Properties von DirectoryInfo und FileInfo sich weitestgehend gleichen und nur einige Spezifika überschiedlich sind.

Das liegt nicht nur daran, dass weitestgehend ähnliche Informationen notwendig sind, sondern beide Klassen von der gleichen Basisklasse FileSystemInfo abgeleitet sind. So etwas nennt man Vererbung, der Vererbungspfad für die Klasse FileInfo ist hier zur Veranschhalichung dargestellt:

System.Object
System.MarshalByRefObject
System.IO.FileSystemInfo
System.IO.FileInfo

 

Der Vollständigkeit halber hier noch die Codeunit 60000 “File Helper” als Ganzes, inklusive der bisher noch fehlenden Prozedur AddFileEntry() :

 OBJECT Codeunit 60000 File Helper
{
  OBJECT-PROPERTIES
  {
    Date=17.12.10;
    Time=00:00:00;
    Version List=FileHelper;
  }
  PROPERTIES
  {
    OnRun=BEGIN
          END;

  }
  CODE
  {

    PROCEDURE FillFileTable@1010001(VAR TempFile@1010000 : TEMPORARY Record 2000000022) : Boolean;
    VAR
      DirectoryInfo@1010002 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.IO.DirectoryInfo";
      Directories@1010005 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Array";
      Files@1010006 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Array";
      Directory@1010003 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.IO.DirectoryInfo";
      File@1010004 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.IO.FileInfo";
      Path@1010001 : Text[250];
      i@1010007 : Integer;
    BEGIN
      Path := TempFile.GETFILTER(Path);
      TempFile.SETFILTER(Path, Path + '*');

      DirectoryInfo := DirectoryInfo.DirectoryInfo(Path);
      Directories := DirectoryInfo.GetDirectories();
      Files := DirectoryInfo.GetFiles();

      FOR i := 0 TO Directories.Length() - 1 DO BEGIN
        Directory := Directories.GetValue(i);
        AddFileEntry(TempFile, Directory.FullName(), Directory.Name(),
            FALSE, 0, Directory.LastWriteTime());
      END;

      FOR i := 0 TO Files.Length() - 1 DO BEGIN
        File := Files.GetValue(i);
        AddFileEntry(TempFile, File.FullName(), File.Name(),
            TRUE, File.Length(), File.LastWriteTime());
      END;
    END;

    LOCAL PROCEDURE AddFileEntry@1010010(VAR TempFile@1010005 : TEMPORARY Record 2000000022;FullName@1010000 : Text[250];
               Name@1010001 : Text[250];IsFile@1010002 : Boolean;Size@1010003 : Integer;LastAccess@1010004 : DateTime);
    BEGIN
      TempFile.INIT;
      TempFile.Path := FullName;
      TempFile."Is a file" := IsFile;
      TempFile.Name := Name;
      TempFile.Size := Size;
      TempFile.Date := DT2DATE(LastAccess);
      TempFile.Time := DT2TIME(LastAccess);
      TempFile.INSERT;
    END;

    BEGIN
    END.
  }
}

Ein Ergebnis könnte dann beispielsweise so aussehen, gefiltert auf mein unaufgeräumtes Temp-Verzeichnis. Tatsächlich sieht mein Verzeichnis wesentlich schlimmer aus, Sie sehen hier max. 5% des Unheils Smile

image

Carsten Scholling

Microsoft Dynamics Germany
Microsoft Customer Service und Support (CSS) EMEA

Email: cschol@microsoft.com
Online Support: www.microsoft.com/support
Sicherheitsupdates: https://www.microsoft.com/germany/athome/security

Microsoft Deutschland GmbH
Konrad-Zuse-Straße 1, D - 85716 Unterschleißheim
https://www.microsoft.com/germany