ModelMaker is a fabulous round-trip UML tools for Borland's Delphi. This paper describes how I created a custom design critic.
I have the need to ensure that classes, interfaces, and members have some form of documentation, i.e. they must have either the documentation box or the "one-liner" field filled in. This is something that neither of the existing critics can do.
Plug-ins are DLLs placed in the Experts directory under the place where ModelMaker was installed. When one starts ModelMaker, it automatically examines the DLLs and loads them if they have the correct entry points.
MMExperDemo.zip file in the Experts directory.
MMToolsApi to the project uses clause.
InitializeExpert class is commented out for now.
Export the three entry points with the correct name.
(All of this code is a stripped-down version of the demo code.)
procedure InitializeExpert(const Srv: IMMToolServices); stdcall;
begin
// (Required action. See demo code.)
MMToolServices := Srv;
// Add design critic
//TODO: Create critic class and uncomment this next line.
//MMToolServices.CriticManager.AddCritic(TsdSomeDocCritic.Create);
// Sync with parent window, (Required action. See demo code.)
Application.Handle := Srv.GetParentHandle;
end;
procedure FinalizeExpert; stdcall;
begin
end;
function ExpertVersion: LongInt; stdcall;
begin
// (Required action. See demo code.)
Result := MMToolsApiVersion;
end;
exports
InitializeExpert name MMExpertEntryProcName,
FinalizeExpert name MMExpertExitProcName,
ExpertVersion name MMExpertVersionProcName;
end.
uses clause.
MMToolsApi and MMCriticsBase units to the interface uses clause.
unit sdCritic;
interface
uses
MMToolsApi,
MMCriticsBase;
TMMDesignCritic.
This class is the design critic.
type
TsdSomeDocCritic = class(TMMDesignCritic)
end;
Category property.
(As far as I can tell, the value is arbitrary.)
type
TsdSomeDocCritic = class(TMMDesignCritic)
public
constructor Create;
end;
implementation
constructor TsdSomeDocCritic.Create;
begin
inherited Create;
Category := 'Documentation';
end;
ModelMaker displays this value in the Critic Manager thus:
GetAuthor, GetCriticName, GetDescription, and GetHeadline public methods.
The GetCriticName method should return something unique from what I understand in the documentation.
type
TsdSomeDocCritic = class(TMMDesignCritic)
public
constructor Create;
function GetAuthor: WideString; override; safecall;
function GetCriticName: WideString; override; safecall;
function GetDescription: WideString; override; safecall;
function GetHeadLine: WideString; override; safecall;
end;
implementation
constructor TsdSomeDocCritic.Create;
begin
inherited Create;
Category := 'Documentation';
end;
function TsdSomeDocCritic.GetAuthor: WideString;
begin
Result := 'IFM Services, LLC';
end;
function TsdSomeDocCritic.GetCriticName: WideString;
begin
Result := 'Non-zero Documentation Critic';
end;
function TsdSomeDocCritic.GetDescription: WideString;
begin
Result := 'This critic ensures that classes and members have one-line or multi-line documentation (inclusive or).';
end;
function TsdSomeDocCritic.GetHeadLine: WideString;
begin
Result := 'Check for some documentation';
end;
ModelMaker displays the GetAuthor, GetDescription, and GetHeadline methods in the Critic Manager thus:
Now that there's a class, we can fix up the TODO item left in the project file:
procedure InitializeExpert(const Srv: IMMToolServices); stdcall; begin // (Required action. See demo code.) MMToolServices := Srv; // Add design critic MMToolServices.CriticManager.AddCritic(TsdSomeDocCritic.Create); // Sync with parent window, (Required action. See demo code.) Application.Handle := Srv.GetParentHandle; end;
At this point we should have a valid design critic that does absolutely nothing. Now we add a method that examines each class, interface, and member, ensuring each has some form of documentation. Any class, interface, member that has no documentation at all will be reported back to ModelMaker.
type
TsdSomeDocCritic = class(TMMDesignCritic)
public
constructor Create;
function GetAuthor: WideString; override; safecall;
function GetCriticName: WideString; override; safecall;
function GetDescription: WideString; override; safecall;
function GetHeadLine: WideString; override; safecall;
procedure Refresh; override; safecall;
end;
MMEngineDefs to the unit implementation uses clause.
The MMEngineDefs unit contains various type and constant definitions we need to examine elements in the ModelMaker project.
implementation
uses
MMEngineDefs;
IMMClassBase interface.
This allows us to attack both classes and interfaces in a simple manner.
IUnknown and TObject.
procedure TsdSomeDocCritic.Refresh;
procedure AddMessage(aEntity :IInterface; aMessage :WideString);
var
lNewMessage: IMMMessage;
begin
// This method reports a problem to ModelMaker
// (Taken directly from demo.)
lNewMessage := MMToolServices.MessageServer.CreateMessage(CriticID, MMCriticsContainer);
lNewMessage.HeadLine := aMessage;
lNewMessage.Priority := Priority;
lNewMessage.Category := Category;
lNewMessage.ReferToEntity(aEntity);
end;
var
i, j: Integer;
lCodeModel: IMMCodeModel;
lClass: IMMClassBase;
lClassName: WideString;
lMember: IMMMember;
lMemberName :WideString;
begin
// All of the setup and teardown code came from the demo.
lCodeModel := MMToolServices.CodeModel;
MMToolServices.MessageServer.BeginUpdate;
try
MMToolServices.MessageServer.DeleteOwner(CriticID);
//
// Examine each class
//
for i := 0 to Pred(lCodeModel.ClassCount) do
begin
lClass := lCodeModel.Classes[i];
if not Assigned(lClass) or not lClass.Valid then Continue;
//
// Skip placeholder classes completely
//
if lClass.Options[classPlaceHolder] then Continue;
//
// Check class' documentation.
//
lClassName := lClass.Name;
if (lClass.OneLiner='') and (lClass.Documentation='') then
AddMessage(lClass, Format('Class %s has no documentation', [lClassName]));
//
// Examine each class member
//
for j := 0 to Pred(lClass.MemberCount) do
begin
lMember := lClass.Members[j];
if not Assigned(lMember) or not lMember.Valid then Continue;
//
// Check member's documentation.
//
lMemberName := lMember.Name;
if (lMember.OneLiner='') and (lMember.Documentation='') then
AddMessage(lMember, Format('Member %s.%s has no documentation', [lClassName, lMemberName]));
end;
end;
finally
MMToolServices.MessageServer.EndUpdate;
end;
end;
The following simple steps add the critic to ModelMaker.
To use, open the Messages window, click on Critics, and press the refresh button.
Debugging a critic is the same as debugging any DLL in Delphi.
Run | Run Parameters menu item.
Host Application, select the ModelMaker executable, e.g. C:\Program Files\ModelMaker\ModelMaker\6.1\bin\mm6.exe.
-nosplash command line parameter in the lower edit box for convenience.
Sample source files are here.
Sample source files for the enhanced version are here.
I've since enhanced this critic to be a little more intelligent, and to handle the specific needs of the project. These enhancements have been pretty straightforward so far. For example:
Here is code to modify the TsdSomeDocCritic.Refresh method (above) that will ignore constructors, destructors, property fields, and property methods:
var
..
lField: IMMField;
lMethod :IMMMethod;
...
//
// Examine each class member
//
for j := 0 to Pred(lClass.MemberCount) do
begin
lMember := lClass.Members[j];
if not Assigned(lMember) or not lMember.Valid then Continue;
//
if lMember.MemberType=cpMethod then
begin
lMethod := lMember as IMMMethod;
// Skip constructors and destructors
if lMethod.MethodKind in [mkConstructor, mkDestructor] then
Continue;
// Skip property methods
if lMethod.Options[methodAccessMethod] then
Continue;
end
else if lMember.MemberType=cpField then
begin
lField := lMember as IMMField;
// Skip property fields
if lField.Options[fieldReserved3] then
Continue;
end;
//
// Check member's documentation.
//
lMemberName := lMember.Name;
if (lMember.OneLiner='') and (lMember.Documentation='') then
AddMessage(lMember, Format('Member %s.%s has no documentation', [lClassName, lMemberName]));
end;
This web page was designed to give a short tutorial. I make no guarantee that any of this information is correct. If this does work, or if you break your computer trying to use this information, that's the risk you take.
If you have comments, wish to report a mistake, or make any other suggestion, please send e-mail. I will try to respond as soon as possible.