Adding a custom Ocl Operation to ECO
Some Background Info
To get myself acquainted with ECO and ASP.NET, I started with a simple (but long time overdue) pet project: a web based bug reporting utility. Noting fancy, mind, just a way for my users to report bugs or feature requests, and a way for myself to mark submitted reports as "opened", "closed" or, as is often the case, "needs more info". I'll be blogging about my first experiences with ECO at a later date.In the model for my bug reporting tool, I have a "Session" class, a transient singleton with an association "CurrentUser" to a "User" class. That makes it possible to refer to the current user with the Ocl expression: "Session.AllInstances->first.CurrentUser".
Soon I realised that I was using this Ocl expression almost everywhere. Especially in the state diagram for reports, where I wanted to express transition guards like "a submitted report can only be deleted by the submitter" (Session.AllInstances->first.CurrentUser = User), or "only an administrator can mark a submitted project as open" (Session.AllInstances->first.CurrentUser.IsAdministrator). This certainly had the duplicate code smell all over it.
Asking about this in the ECO newsgroup, Oleg Zhukov suggested to define a custom Ocl operation (Thanks Oleg!).
Defining the custom operation
Defining a custom Ocl operation is not as complicated as it might seem (although it can take some time finding the relevant documentation on the net). It involves overriding two methods from the base class "OclOperationBase", and installing the derived operation into the oclservice. Let's start with the interface: uses
Borland.Eco.Ocl.Support;
type
OclCurrentUser =
class
(OclOperationBase)strict protected
procedure
Init; override
;public
procedure
Evaluate(Parameters: IOclOperationParameters); override
;end
;Not much to see there, so on to the implementation. In the overridden Init method, we can define the number of parameters our custom Ocl operation will take and set their types. An Ocl operation always needs at least one parameter. This parameter represents the context of the Ocl operation. In this case we want to be able to use the "CurrentUser" operation with any type of context.
To complete the initialisation we call the InternalInit method, providing a name, the input parameter array, and the return type for our custom Ocl operation. In most of the examples that are floating round the net the return type is a predefined type like Support.StringType or Support.IntegerType. In our case however, we need to return a user object ("user" being a class in the model). We get the Ocl type for our user class by using the GetClassifierByType() and GetOclTypeFor() functions.
uses
Borland.Eco.UmlRt;
procedure
OclCurrentUser.Init;var
OCLParameters:
array
of
IOclType;Classifier: IClassifier;
OclType: IOclType;
begin
SetLength(OclParameters, 1);
OclParameters[0] := Support.AnyType;
Classifier := Support.Model.GetClassifierByType(Typeof(User));
OclType := Support.GetOclTypeFor(Classifier);
InternalInit(
'currentuser'
, OclParameters, OclType);end
;That's the initialisation done. Now let's get on with the evaluation. We already know the Ocl expression to return the current user, so we can just call the Ocl service to evaluate this expression.
The last task is setting the result parameter. We have two options: SetOwnedElement() and SetReferenceElement(). It's my feeling that SetOwnedElement() should be used for returning objects created by the operation itself, so that in our case SetReferenceElement() is the way to go. However, both options seem to work without problems (and documentation is lacking), so perhaps someone can shed some light on this?
uses
Borland.Eco.ObjectRepresentation,
Borland.Eco.Services;
procedure
OclCurrentUser.Evaluate(Parameters: IOclOperationParameters);var
OclExpression:
string
;OclService: IOclService;
Element: IElement;
begin
OclExpression :=
'Session.AllInstances->first.currentuser'
;OclService := Support.OclService;
Element := OclService.Evaluate(OclExpression);
Parameters.Result.SetReferenceElement(Element);
end
;Installing the custom operation in run- and designtime
All the above takes care of defining our custom operation. Installing it into the OclService is a breeze. Just add one line of code to the constructor of the EcoSpace:constructor
TBugReportsEcoSpace.Create;begin
inherited
Create;InitializeComponent;
// TODO: Add any constructor code here
OclService.InstallOperation(OclCurrentUser.Create);
end
;Are we all done now? Well, almost. Although the custom operation is now known in runtime, and functions as expected, the operation is not yet known in design time. This has as a side effect that when we try to validate a model using the custom operation, we'll get an error: "undefined operation: CurrentUser". Luckily that's easily solved. By adding an attribute to our EcoSpace we can make the Ocl operation known at design time:
[EcoOclOperation(typeof(OclCurrentUser), true)]
TBugReportsEcoSpace =
...
TBugReportsEcoSpace =
class
(Borland.Eco.Handles.DefaultEcoSpace)private
...
Side note: in this article about the same subject, Jesper Hogstrom remarks "Due to a very small. Minor... Miniscule... let's call it oversight, the attribute cannot be applied twice", and provides an alternative solution if you want to add more than one design time Ocl operation. It seems however that in the meantime this oversight has been fixed.
Calling the custom operation
When calling the custom operation, there are two "gotchas" to be beware of. The first is not forgetting the parentheses (i.e. calls must be in the form "CurrentUser()"), and the second is that an Ocl operation always needs a context. Although (for this operation) it doesn't matter what the context is, a context must be provided. When using the custom operation in a state diagram, the context is understood to be the class we're designing the state diagram for, so we can use "CurrentUser()" on its own. When evaluating the custom operation in code, we have to provide the context ourselves. // context can be anything, but must be provided
Element := OclService.Evaluate(
'42.CurrentUser()'
);Currentuser := User(Element.AsObject);