Catch Me If You Can – try-catch für .NET in C/AL
In diesem Artikel geht es nicht um Frank W. Abagnale Jr., den einzigen Teenager, der es je auf die FBI-Liste der zehn meistgesuchten Kriminellen in den Vereinigten Staaten gebracht hat (siehe auch https://www.kino.de/kinofilm/catch-me-if-you-can/66581), sondern um die Ausnahmebehandlung bei der Nutzung von .NET-Variablen in Dynamics NAV.
Jeder der schon einmal etwas mehr im Bereich der Interoperabilität gemacht hat, wird unweigerlich darauf gestoßen sein, dass eine bei Nutzung von DotNet-Klassen auftretende Ausnahme/Exception, die weitere Ausführung des C/AL-Programmcodes verhindert. Das gleiche Verhalten also, wie bei der Nutzung von ERROR(). Die Fehlermeldung wird ausgegeben und die Verarbeitung beendet. Das wird allgemein genutzt, um Fehleingaben zu quittieren oder andere nicht behandelbare Probleme eindeutig abzufangen. Der Vorteil ist, dass Sie im C/AL selbst bestimmen können, wann ein Abbruch der Verarbeitung sinnvoll ist:
IF kaputt THEN BEGIN
Aufraeumen();
ERROR('kaputt!');
END;
Im .NET Framework dient eine Ausnahme ebenfalls genau dazu. Allerdings ist es nicht immer möglich, alle Eventualitäten zu kalkulieren. Prüfen Sie beispielsweise vor dem Schreiben einer Datei per .NET aus C/AL heraus den Zugriff auf einen Pfad, kann dies, einige Millisekunden später, wenn die Ausgabe der Datei über eine .NET-Klasse erfolgen soll, schon nicht mehr so sein. Sei es, dass der Pfad nicht länger existiert, die Berechtigung sich zur Millisekunde geändert hat, ein ferner Computer heruntergefahren wurde oder hunderte andere Dinge, die man sich als Programmierer lieber nicht vorstellt. Jede noch so gut geplante Implementierung und Vorabprüfung wird durch eines immer wieder ausgehebelt: Den Menschen, dessen freien Willen und unendliche Kreativität!
Aber nehmen wir einfach mal an, der Chefentwickler im Hause hat alle, aber auch absolut alle Eventualitäten berücksichtigt. Vom heruntergefahrenen Server, über die Zugriffsrechte, bis hin zum Kaffeekonsum des Administrators. Nehmen wir an, es gäbe eine Methode (Funktion) in einer .NET-Klasse, die auf alles erdenkliche vorbereitet ist. Was tut nun aber eine klassische .NET-Implementierung in dem Fall, dass zwar alles abgefangen und geprüft wird, aber letztendlich festgestellt werden muss, dass die übergebenen Daten nicht passen? Es wird eine Ausnahme erzeugt, die den Aufrufer über die fehlerhaften Daten informiert. Ist der Aufrufer ebenfalls in z.B. C# implementiert, wird dieser (wahrscheinlich) das try/catch-Konstrukt zur Fehlerbehandlung nutzen und dort dann reagieren. Ist der Aufrufer allerdings Dynamics NAV bzw. dortiger C/AL-Code, führt diese Ausnahme zu einem Fehler, die Ausführung wird unterbrochen und die Standardausnahmebehandlung inkl. Datenbank-Rollback wird ausgeführt.
Man darf an dieser Stelle nicht vergessen, dass Dynamics NAV, ob Server oder Client, vollständig in .NET implementiert ist und auch C/AL beim kompilieren im Object Designer in C#-Code übersetzt und später zur Laufzeit in eine DLL kompilert wird, diese Exception also innerhalb einer Klasse PageXY auftritt, in einer Methode ActionXY, in der der C/AL-Entwickler eine DotNet-Variable benutzt hat. Und das alles nur, weil ein Benutzer, seinem freien Willen uneingeschränkt folgend, und ohne Feld XY auszufüllen, auf der Page XY eine Action XY angeklickt hat… Das bedeutet falsche Daten, interne Prüfung, Methode “wehrt sich”, Methode erzeugt Ausnahme, was sich dann so äußert:
ERROR('kaputt!');
Auch wenn C/AL und .NET letztendlich im selben C#-Code landen, müssen nach Auftreten von Fehlern (ich meine jetzt aber explizit nicht ERROR() oder eine Ausnahme), eventuell Dinge aufgeräumt, Daten zurückgesetzt oder ein entsprechendes Protokoll geschrieben werden. Das funktioniert aber nur, wenn eine Ausnahme auch von außen (C/AL) abgefangen werden kann.
try-catch
C/AL unterstützt (noch) kein try-catch-finally-Konstrukt. Eine Möglichkeit wäre also, den Aufruf einer .NET-Methode per try-catch in einer eigenen .NET-Methode zu kapseln und diese neue Methode aufzurufen, die den Fehler sauber abfängt. Dies ist sicherlich sinnvoll für Eigenentwicklungen, aber wer möchte schon für den Fundus von zehntausenden von .NET-Klassen jeweils eine Aufruffunktion programmieren? Das wird vom Entwickler sicherlich direkt bei Anforderung durch Kopfschütteln, ein klares “NEIN!” oder auch andere, den Mittelfinger nutzende Gesten, quittiert.
Die Antwort: Generische Herangehensweise! Das .NET Framework bietet unter anderem auch einen Mechanismus mit Namen “Reflection”. Reflection erlaubt es, .NET-Assemblies zu analysieren, Informationen zu Klassen, Schnittstellen und Wertetypen auszulesen, Instanzen zur Laufzeit zu erzeugen oder Eigenschaften und Methoden ab- oder aufzurufen. Es ist also auch möglich, die Methode einer Klasse direkt mit Namen aufzurufen. Ein Beispiel dazu, wobei classObject eine Klasse vom Typ Decimal ist:
Type objectType = classObject.GetType();
MethodInfo method = objectType.GetMethod("Divide");
method.Invoke(classObject, new object[] { 1, 0 });
Der zweite Parameter von method.Invoke() ist ein Array der übergebenen Parameter (Divident und Divisor) und führt hier natürlich unweigerlich zu einer Ausnahme “Division durch null”. Da wir genau das nicht wollen, was wäre mit folgender Lösung?
try
{
Type objectType = classObject.GetType();
MethodInfo method = objectType.GetMethod("Divide");
method.Invoke(classObject, new object[] { 1, 0 });
}
catch(Exception)
{
}
Das funktioniert ohne Abbruch, da die auftretende Ausnahme durch den catch abgefangen wird. Klar wird es eine DivideByZeroException sein, diese erbt aber von Exception, so dass wir mit obigem catch(Exception) die meisten, aber nicht alle Ausnahmen abfangen können. Beispiele für nicht-abfangbare Ausnahmen sind OutOfMemoryException oder StackOverflowException. Es ist eher unwahrscheinlich, dass diese im Benutzer-Code noch korrigiert werden können.
Aber wieder zurück zum Thema: Kapseln wir nun das obige Beispiel in einer eigenen Methode, und übergeben die Klasse/das Objekt, den Methodennamen und die Aufrufparameter als Parameter dieser Methode, dann ist das schon recht generisch. Allerdings müssen wir das Ganze noch ein wenig aufwerten. Denn was passiert, wenn eine Methode überladen ist, es also mehrere Implementierungen (mit verschiedenen Parametern) gibt? Ich denke Sie erraten es: Ein Fehler, denn die Methode kann nicht eindeutig bestimmt werden. Das bedeutet also eine Exception, genau: eine AmbiguousMatchException.
Dafür wiederum gibt es Überladungen der Methode GetMethod() . Eine, die meiner Meinung nach ausreichend zur Identifikation der gewünschten Methodensignatur ist, erwartet zwei Parameter. Den Namen und ein Type[] -Array, mit dem die Art der Parameter übergeben werden kann. Methodensignaturen unterscheiden sich nämlich alle in Menge und Reihenfolge der übergebenen Parameter-Varianblentypen. Daraus ergibt sich dann eine etwas umfangreichere Variante.
public bool InvokeMethod(object classObject, string methodName, List<object> parameters)
{
try
{
List<Type> parameterTypes = new List<Type>();
foreach(object parameter in parameters)
{
parameterTypes.Add(parameter.GetType());
}
Type objectType = null;
if (classObject is Type)
{
objectType = classObject as Type;
classObject = null;
}
else
{
objectType = classObject.GetType();
}
MethodInfo method = objectType.GetMethod(methodName, parameterTypes.ToArray());
if (method == null)
{
throw new AmbiguousMatchException();
}
LastReturnValue = method.Invoke(classObject, parameters.ToArray());
}
catch (Exception ex)
{
InitializeExceptionProperties(ex);
return false;
}
return true;
}
Aus den übergebenen Parametern, die aus Dynamics NAV als generische Liste übergeben werden, wird wiederum eine generische Liste der Typen erstellt, die zur Ermittlung an die Methode GetMethod() übergeben wird. Sie sehen, dass obiger Code noch eine kleine Variante enthält. Es existieren statische und nicht-statische Typen, Klassen und Methoden. Statische Methoden gehören zum Typ selbst und nicht zu einer spezifischen Instanz eines Typs. Dementsprechend erlaubt die Variante oben auch die Übergabe eines Typs (ermittelt über den C/AL Befehl GETDOTNETTYPE() ). Wenn gar nichts mehr geht, also eine Methode nicht gefunden wird, dann meldet sich diese Implementierung ebenfalls mit einer Ausnahme zu Wort, hier einer selbst ausgelösten AmbiguousMatchException. Denn in dem Fall, hat offensichtlich der Entwickler etwas übersehen.
Wer nun noch über die Methode InitializeExceptionProperties() gestolpert ist, diese ist im Folgenden aufgeführt. Zusammen mit GetFullExceptionMessage() , welche bis zur innersten Ausnahme alle Ausnahmen-Fehlertexte konsolidiert, werden hier die Ausnahme und der Fehlertext zum Abruf aus C/AL gespeichert und der letzte Rückgabewert auf null gesetzt.
private void InitializeExceptionProperties(Exception exception = null)
{
LastException = exception;
LastMessage = exception != null ? GetFullExceptionMessage(exception) : string.Empty;
LastReturnValue = null;
}
private static string GetFullExceptionMessage(Exception exception)
{
string errorMessage = string.Empty;
Exception exception2 = exception;
do
{
errorMessage += string.Format(" - {0}", exception2.Message);
exception2 = exception2.InnerException;
}
while (exception2 != null);
errorMessage = errorMessage.Substring(3);
return errorMessage;
}
Da ich eigentlich schon wieder zu viel geschrieben habe, möchte ich die zwei übrigen und letzten Methoden der Klasse, die ich übrigens Generic Object Handler, kurz GenericObjHandler getauft habe, ohne große Erläuterungen vorstellen:
public object GetProperty(object classObject, string propertyName)
{
InitializeExceptionProperties();
try
{
PropertyInfo property = classObject.GetType().GetProperty(propertyName);
if (property.CanRead)
{
LastReturnValue = property.GetValue(classObject, null);
}
}
catch (Exception ex)
{
InitializeExceptionProperties(ex);
}
return LastReturnValue;
}
public bool SetProperty(object classObject, string propertyName, object propertyValue)
{
try
{
PropertyInfo property = classObject.GetType().GetProperty(propertyName);
if (property.CanWrite)
{
property.SetValue(classObject, propertyValue, null);
}
}
catch (Exception ex)
{
InitializeExceptionProperties(ex);
return false;
}
return true;
}
Nur so viel vielleicht: Neben Methoden kann man über Reflection natürlich auch auf Eigenschaften zugreifen. Sogar private Variablen und Anderes sind möglich. Für Eigenschaften wird die PropertyInfo anhand des Namens ermittelt, geprüft ob diese gelesen oder geändert werden kann, und dies dann entsprechend mit GetValue() oder SetValue() durchgeführt.
Aufruf aus Dynamics NAV C/AL
Und wie wird diese Klasse nun benutzt? Dazu habe ich folgende, eigentlich selbsterklärende Codeunit geschrieben. Diese dient primär dazu, den Aufruf des GenericObjHandler zu kapseln und Methodenaufrufe mit verschiedener Anzahl an Parametern einfacher zu gestalten, so dass nicht zwingend eine generische Liste erzeugt werden muss. Ich möchte nich darauf hinweisen, dass in meinen Tests bei der Nutzung von Variant-Datentypen für DotNet-Variablen einige “Merkwürdigkeiten” auftraten, weswegen ich im angehängten Archiv, die Parameter vorab einer DotNet-Variablen zuweise und diese an die Parameterliste übergebe.
Ich habe einige Beispiele für die Nutzung in eine Codeunit verpackt, möchte hier aber stellvertretend nur zwei Anwendungsfälle zeigen.
Es gbt jeweils zwei Varianten der Funktionen, eine die eine Ausnahme hervorruft und damit die Verarbeitung abbricht und eine zweite (mit Suffix “TryCatch”), welche über den GenericObjHandler arbeitet und eben nicht abbricht, da die Ausnahme abgefangen wird.
Im ParseXml() geht es um die Validierung einer XML-Datei gegen ein Schema. Hier tritt beim Load() eine XmlException auf. Mit der Implementierung ParseXmlTryCatch() wird diese über den GenericObjHandler abgefangen und der Fehler angezeigt.
Ähnliches gilt für DivisionByZero() und DivisionByZeroTryCatch() . im letzteren Fall wird die Ausnahme angefangen und der entsprechende Fehlertext zurückgegeben.
Installation
Die Installation ist recht einfach. Laden Sie sich das angehängte Archiv herunter. Dieses enthält
- das Visual Studio 2013 Projekt für die GenericObjHandler-DLL,
- die Dynamics NAV Codeunits 50011 “Try Catch” und 50010 “Test Try Catch”
- und 3 Testdateien für die XML-Validierung (TryCatch.xsd, TryCatch_Error.xml, TryCatch_Ok.xml).
- Kopieren Sie, nach der Kompilierung des VS-Projekts, die DLL in die jeweiligen Add-In Verzeichnisse. Für den Dynamics NAV 2013 R2 Server unter C:\Program Files\Microsoft Dynamics NAV\71\Service\Add-ins und den zugehörigen Client unter C:\Program Files (x86)\Microsoft Dynamics NAV\71\RoleTailored Client\Add-ins. Selbstverständlich können Sie auch ein Unterverzeichnis anlegen.
- Importieren Sie die Objekte aus TryCatch_Objects.txt. Kompilieren Sie diese im Object Designer.
- Für den Test der XML-Validierung müssen die 3 Dateien TryCatch.xsd, TryCatch_Error.xml und TryCatch_Ok.xml unter C:\Temp liegen, Sie können den Pfad aber auch einfach in der Codeunit 50010 anpassen.
Un nun wünsche Ich Ihnen auch hier viel Spaß bei der Implementierung und beim Testen. Über Feedback und Weiterentwicklungen freue ich mich und werde diese gerne hier zur Verfügung stellen.
Just for fun!
Zur Feier des Tages noch ein wenig verordneter Spaß
Carsten Scholling
Microsoft Dynamics Germany
Microsoft Global Business Support (GBS) EMEA
Microsoft Connect: https://connect.microsoft.com
Online Support: https://www.microsoft.com/support
Sicherheitsupdates: https://www.microsoft.de/sicherheit
Microsoft Deutschland GmbH
Konrad-Zuse-Straße 1
D-85716 Unterschleißheim
https://www.microsoft.de
Comments
Anonymous
September 28, 2014
Ich bin ja ein Fan von solchen Lösungen (Habe schon ähnliches gebaut um nicht eindeutige Methoden aufrufen zu können, oder Generic Methoden) , aber so etwas würde ja dem fxcop test einer CfMD Prüfung nicht standhalten da generelle Exceptions abgefangen werden (was einem critical error entspricht)?Anonymous
October 01, 2014
The comment has been removedAnonymous
November 03, 2014
Ich möchte gerne auf eine elegantere Lösung zum Try/Catch von Vjekoslav Babić hinweisen: vjeko.com/.../try-catch-for-net-interoperability. Ich denke die werde ich zukünftig auch verwenden :) Der Pokal geht damit an Vjekoslav! Viele Grüße, Carsten SchollingAnonymous
November 11, 2014
Ich zuerst entschuldige mich, daß ich sofort auf Englisch umschalten muß. Thanks, Carsten! I have actually demonstrated the Try..Catch concept last year at the NAV TechDays and I have only taken bits and pieces of it out into the blog post I made a couple of weeks ago. Thanks for referring back to it! I really appreciate it. Keep up the great work, VjekoAnonymous
November 11, 2014
Hi Vjeko, Danke/Thank you! :) I had this Topic on my Agenda for some months, but after reviewing your solution, I decided that your's is the better approach. CU on your blog. Cheers CarstenAnonymous
January 14, 2015
Schöne Möglichkeit, hab angefangen zu lesen und dann selbst drauf los gecoded - das Resultat war interessanterweise fast wie das von dir :) Die Lösung von Vjekoslav finde ich nicht so gut, weil man beim Catch auswerten muss an welcher Stelle man war (gibt ja nur ein Event) um darauf zu reagieren. Was generell blöd ist: NAV bekommt es nicht gebacken jeden spezifischen Typ als Object anzusehen, also muss man als Return immer eine Variable des Typs Objekt nehmen und dann eine einfache Zuweisung in NAV machen (wie casten).