Meer bedoeld als tip dan als artikel, al blijkt het toch een aardige lap tekst geworden...
Intro
Altijd een groot vraagstuk: hoe toon je nou een exception aan je gebruiker. Je wilt niet dat de gebruiker schrikt van een technisch rapport, maar je wilt die informatie zelf wel kunnen zien.
Detail-schermpje (of loggen)
Vaak wordt er dan gegrepen naar een soort 'Detail' scherm: de gebruiker krijgt een algemene melding, en kan op 'Details' klikken om de rest te zien. Het valt te betwijfelen of die oplossing vanuit een UX-design standpunt nou zo geweldig is, maar het opent in ieder geval de weg naar het loggen van de details en het tonen van een gebruikersvriendelijke melding.
Zelf heb ik ook nog zo'n details-schermpje rondhangen. Met behulp van een TApplicationEvents object wordt het OnException event afgevangen, en worden exceptions daar apart verwerkt. Bij bepaalde exceptions toon ik een afwijkend venstertje dat de details toont. Bovendien kun je daar ook inspringen op bepaalde exception classes, zodat je ook de aanvullende informatie (zoals de errorcode van een ADO exception) kunt loggen.
Helaas is dat ook niet al te makkelijk. Je wilt de gebruikersvriendelijke melding ook wel een beetje specifiek hebben, en geen 'Er is iets fout gegaan' in je algemene exception handler zetten. Je hebt dus een exception class nodig waarin je de gebruikersmelding en de technische melding kunt zetten.
DetailException
Je zou je voor kunnen stellen dat je een detail-exception maakt waarin je de aanvullende informatie apart kunt meegeven:
Delphi Code:
type
EDetailedException = class(Exception)
// Alle constructors, zodat er een detail-rapport aan
// meegegeven kan worden.
end;
Een leuk begin, maar dan moet je steeds de vorige exception gaan uitlezen en meegeven. Eventueel kun je de constructor nog uitbreiden dat deze ook een exception slikt, maar dat maakt het er nog niet per se makkelijker op. Exception heeft zelf een stuk of 10 constructors en bovendien raak je dan mogelijk allerlei details kwijt over de aard van de message. Bepaalde message classes bevatten tenslotte aanvullende informatie, en je wilt die ook weer niet op elke plaats apart moeten verwerken.
EExceptionForward
Nou zocht ik eigenlijk een manier om een exception mee te geven aan een andere exception, en deze eigenlijk als het ware in te pakken:
Delphi Code:
type
EExceptionForward = class(Exception)
property OriginalException: Exception;
end;
Op die manier zou ik een EExceptionForward kunnen gooien, die verwijst naar de oorspronkelijke exception. Helaas, wanneer een Exception object is geraised, zal het automatisch vrijgegeven worden aan het eind van het except-blok. In Application.OnException is OriginalException dus al vrijgegeven.
De oplossing: AcquireExceptionObject
AcquireExceptionObject is een functie die het laatst geraisde exception object teruggeeft, en voorkomt dat deze automatisch vrijgegeven wordt. De functie is al een enkele keer langsgekomen op NLDelphi, maar wordt in het algemeen weinig gebruikt. Hij is echter wel handig te gebruiken in zo'n EExceptionForward object, door in de constructor het vorige exception object op te vragen. Vanaf dat moment is het EExceptionForward object ook de eigenaar van het exception object, en dus verantwoordelijk voor het vrijgeven ervan.
AcquireExceptionObject kan aangeroepen worden in de AfterConstruction method, zodat je niet alle constructors van Exception hoeft te overriden.
Delphi Code:
procedure EExceptionForward.AfterConstruction;
begin
inherited;
OriginalException := AcquireExceptionObject;
end;
destructor EExceptionForward.Destroy;
begin
OriginalException.Free;
inherited;
end;
Ik zie er wel brood in dat je een exception kan geven waarbij je zelf de details opgeeft, maar ook een variant die de details zelf bepaalt op basis van een andere exception. Daarom een base class die MainMessage (= Message) en DetailMessage als property implementeert. De property FullMessage geeft een combinatie van die twee terug. EExceptionForward hoeft alleen GetDetailedMessage te overriden om de details van de originele message op te vragen.
Delphi Code:
type
ECustomDetailedException = class(Exception)
...
public
property FullMessage: String read GetFullMessage;
property MainMessage: String read GetMainMessage;
property DetailedMessage: String read GetDetailedMessage write SetDetailedMessage;
end;
EDetailedException = class(ECustomDetailedException)
// Hier ruimte voor het overriden van alle constructors, zodat er een detail-rapport aan
// meegegeven kan worden.
end;
EExceptionForward = class(ECustomDetailedException)
...
protected
function GetDetailedMessage: String; override;
...
Toepassing
Het doorsturen van zo'n exception is dan heel eenvoudig:
Delphi Code:
try
TFileStream.Create('::\/\Niet bestaande file***...', fmOpenRead);
except
raise EExceptionForward.Create('Het bestand kon niet worden gelezen.');
end;
In de code hierboven zal EExceptionForward impliciet de geraisede exception opzoeken en 'claimen'. Als hier verder niets mee gedaan wordt, zal de gebruiker alleen de melding 'Het bestand kon niet worden gelezen.' te zien krijgen.
Als je wél wat met die details wilt doen, kun je het Application.OnException event gebruiken. Daarin kun je controleren op het type van de exception en afhankelijk daarvan een andere melding tonen:
Delphi Code:
procedure TForm2.ApplicationEvents1Exception(Sender: TObject; E: Exception);
begin
if E is ECustomDetailedException then
begin
with ECustomDetailedException(E) do
begin
LogError(FullMessage);
ShowErrorWithDetails(MainMessage, DetailMessage);
end;
end
else
begin
LogError(Message);
Application.ShowException(E);
end;
end;
Het is natuurlijk net zo makkelijk om de DetailMessage (of de FullMessage) te loggen en de exception verder op de gebruikelijke manier te tonen met Application.ShowException.
Ik zou eigenlijk het liefst hebben dat E.Message geoverride wordt, zodat deze de FullMessage teruggeeft. Op die manier kun je dit type exception al makkelijk gaan inzetten zonder de code voor het tonen en loggen van exceptions aan te hoeven passen. Helaas is Exception een klassiek VCL-object waarin niets virtueel is en alles dichtgetimmerd. GetMessage overriden is er dus niet bij.
De code
Delphi Code:
type
ECustomDetailedException = class(Exception)
private
FDetailedMessage: String;
protected
function GetDetailedMessage: String; virtual;
function GetMainMessage: String; virtual;
procedure SetDetailedMessage(const Value: String); virtual;
function GetFullMessage: String; virtual;
public
property FullMessage: String read GetFullMessage;
property MainMessage: String read GetMainMessage;
property DetailedMessage: String read GetDetailedMessage write SetDetailedMessage;
end;
EDetailedException = class(ECustomDetailedException)
// Hier ruimte voor het overriden van alle constructors, zodat er een detail-rapport aan
// meegegeven kan worden.
end;
EExceptionForward = class(ECustomDetailedException)
private
FOriginal: Exception;
protected
function GetDetailedMessage: String; override;
public
property OriginalException: Exception read FOriginal write FOriginal;
procedure AfterConstruction; override;
destructor Destroy; override;
end;
{ ECustomDetailedException }
function ECustomDetailedException.GetDetailedMessage: String;
begin
Result := FDetailedMessage;
end;
function ECustomDetailedException.GetFullMessage: String;
begin
Result := Trim(MainMessage + sLineBreak + DetailedMessage);
end;
function ECustomDetailedException.GetMainMessage: String;
begin
Result := inherited Message;
end;
procedure ECustomDetailedException.SetDetailedMessage(const Value: String);
begin
FDetailedMessage := Value;
end;
{ EExceptionForward }
procedure EExceptionForward.AfterConstruction;
begin
inherited;
OriginalException := AcquireExceptionObject;
end;
destructor EExceptionForward.Destroy;
begin
OriginalException.Free;
inherited;
end;
function EExceptionForward.GetDetailedMessage: String;
begin
Result := inherited GetDetailedMessage;
if (Result = '') and Assigned(FOriginal) then
if OriginalException is ECustomDetailedException then
Result := ECustomDetailedException(OriginalException).GetFullMessage
else
Result := OriginalException.Message;
end;
Voorbeeld
De laatste method hierboven geeft het detail-bericht terug. Als de originele exception ook een EExceptionForward is, dan wordt daar de FullMessage van opgevraagd. Dit resulteert uiteindelijk in een soort call stack van exceptions, bijvoorbeeld deze code:
Delphi Code:
try
try
TFileStream.Create('::\/\Niet bestaande file***...', fmOpenRead);
except
raise EExceptionForward.Create('Het bestand kon niet worden gelezen.');
end;
except
raise EExceptionForward.Create('De import is mislukt.');
end;
Resulteert in deze melding:
De import is mislukt.
Het bestand kon niet worden gelezen.
Cannot open file "::\Niet bestaande file***". The system cannot find the path specified
Samenvatting
- EExceptionForward pakt een exception in, in een meer algemene en/of gebruikersvriendelijke melding.
- EExceptionForward gebruikt AcquireExceptionObject om zelf de getriggerde Exception te bepalen.
- Je kunt ze nesten om een soort stack van fouten te krijgen, van algemeen tot steeds meer detail.
- In Application.OnException kun je een generieke handler maken om exceptions te tonen en/of te loggen.
- Het originele Exception object blijft bewaard binnen de EExceptionForward, zodat je bij het loggen ervan alle informatie nog tot je beschikking hebt.
- Het is simpel.
- Het is fun!
Bookmarks