How to Implement a Default Provider for KBObject Parts

Unofficial Content

Introduction

This tutorial presents a step-by-step guide on how to implement a default provider for a KBObject part. A default provider is simply a class capable of generating the default content for a KBObject or a KBObjectPart instance. For illustration purposes, this tutorial will show how to calculate a default for the Source part of a procedure, in which we will make a call to any other procedure that lacks a parm rule. Additionally, the references to all these procedures will be set as weak, so that the called objects can still be deleted (that is of course, unless they are being referenced elsewhere).

Prerequisites

We will also create and edit a template file, so if you are using Visual Studio 2005, installing the Template Editor Add-in is strongly recommended.

Creating the Project

The very first thing we need is a GXextension in which to implement our default provider, so we will start by creating a new project named DefaultProviderTutorial.

Start Visual Studio 2005 and type Ctrl+Shift+N so as to create a new project. In the Visual C# category, select GeneXus Package as the type of project to create, set the name to DefaultProviderTutorial, and choose the desired location.

CreateExtension

After clicking Ok, the GeneXus Package Wizard will guide you through the creation process. In the first page, set the GeneXus and SDK paths if needed, as well as your company's short and long names.

ExtensionWizardStep1

Clicking Next will, not surprisingly, take you to the second page. We won't be defining any new object type for this extension, so just uncheck that option and click Finish.

ExtensionWizardStep2

At this point, you may want to click F5 in Visual Studio. That will allow you to verify that the project builds with no errors, that it is deployed to the GeneXus X installation, and that GeneXus is able to recognize and load your extension. Use the Extensions Manager option in GeneXus' Tools menu in order to verify that the new extension has been loaded: it should appear in the list of user extensions with a check mark next to it. You can now close GeneXus and go back to Visual Studio.

GXextensionsManager

The initial plumbing

Creating the Default Provider Class

For our default provider class we will need to add a couple of assembly references to the project. Use the "Project / Add Reference" menu option to add references to Artech.Common.dll, Artech.Common.Helpers.dll, and Artech.Genexus.Common.dll, all located in the GeneXus X installation directory.

AddReferences

Use the "Project / Add Class" menu option (or just press Shift+Alt+C) to add a new file named DefaultProvider.cs to the project. In this file, copy the initial version of the class, which you can find here.

As you can see in this initial implementation, a default provider class needs to implement IDefaultProvider, which in turn requires implementing one property and three methods.

The Id property, as its name implies, is a string by which this particular default provider will be referred. As a showoff of our extreme creativity, we will use "DPTutorial" for this.

Default providers may use additional metadata on which to base some decisions when calculating a default. Every time a new KBObjetPart is created, a call to InitialzeProviderData is made for each registered default provider, so that it can set metadata for that part if so desires. Since we will not use this option, however, our implementation of InitialzeProviderData simply sets the providerData parameter to null and returns false.

Whenever the default content of a part is needed, a call is made to the GetTemplate method of each registered default provider along with its associated metadata, if any. In this way, each default provider gets the chance to return a template with which to generate the content. We will later discuss templates, how to write them, and how they are used, but for now, let's just say that we are only interested in calls for parts of ProcedurePart type, and that we might return different templates depending on the content of the metadata. At this point, however, we will only recognize the case for "CreateProcedureCalls", for which we will return a template named "ProcedureCalls.dkt" that we will create in the next step.

The last method we need to implement is UpdateReference, which will also be discussed later in this tutorial. At this stage we return false, thus indicating we are not changing anything.

There are some additional helper methods in our default provider class, but they are pretty much self-explanatory, so we won't cover them in detail.

Creating the Template

Select Add New Item in the Project menu (or press Ctrl+Shift+A) to add a new file to the project and type ProcedureCalls.dkt as its name. Once created, copy the initial version of the template from here.

Template files have template directives (those lines between <%@ and %> delimiters), template code (C# code enclosed with <% and %> delimiters, and template output (any other text, which is copied verbatim to the output). The template output may include expressions (delimited with <%= and %>), which values will be copied as output at generation time.

Templates used to generate the default content of a KBObjectPart receive at least two parameters, the KBObjectPart itself, and the KBObject to which the part belongs. Our initial template declares these two properties using <%@ Property %> directives, although it won't make use of them for now.

The ouput must be an XML fragment that matches the export format for a KBObjectPart (the source part of a procedure in our case). The actual source code will initially be just a comment line. Granted, this isn't terribly useful, but we'll make sure to generate more interesting things as soon as we complete the initial plumbing; be patient, little grasshopper.

The default provider we created in the previous step will direct GeneXus to look for the template file in a particular folder, so we'd better make sure our template gets copied there at deploy time. Edit Catalog.xml in the project, and add the following line, right below the line for DefaultProviderTutorial.dll:

<File Name="ProcedureCalls.dkt" Location="." Target="Packages\DefaultProviderTutorial\Templates" />

For a more detailed explanation on how to write template files take a look at the template syntax.

Registering the Default Provider class

Every default provider class needs to be registered with the DefaultManager. Edit the Package.cs file and add the following using statement:

using Artech.Architecture.Common.Defaults;

Then, inside the Initialize method, right after the call to the base class implementation, add the following line:

DefaultManager.Manager.RegisterDefaultProvider(new DefaultProvider());

Applying the default

So far, we have a template file and a registered default provider that links to it. However, no object is using that particular default, and that's precisely the final piece of the puzzle we will add in this step.

Let's suppose that in every knowledge base we want to have a procedure that calling some other objects, one after the other. We want to use a default to write the source code of this procedure, because this way we don't need to type all those calls (it might be a large list of objects), and because as different objects get added, modified, or deleted, our calling object will be able to automatically update itself by simply recalculating the default. For the sake of simplicity, we will assume that the objects we want to call are all the procedures with no parm rule.

Edit your Package.cs file and add the following using statements:

using Artech.Architecture.Common.Events;

using Artech.Architecture.Common.Objects;

using Artech.Genexus.Common.Objects;

Then add the following methods to the Package class in the same file:

public override void OnAfterOpenKB(object sender, KBEventArgs e)

{

   base.OnAfterOpenKB(sender, e);

   CreateMasterProcedure(e.KnowledgeBase.DesignModel);

}

 

private const string _masterProcName = "MasterProc";

 

private Procedure CreateMasterProcedure(KBModel model)

{

   Procedure proc = Procedure.Get(model, _masterProcName);

   if (proc == null)

   {

      proc = new Procedure(model);

      proc.Name = _masterProcName;

      proc.Parent = Folder.GetRoot(model);

      IApplyDefaultTarget procedurePart = (IApplyDefaultTarget) proc.ProcedurePart;

      procedurePart.IsDefault = true;

      procedurePart.SetProviderData("DPTutorial", "CreateProcedureCalls");

      proc.Save();

   }

 

   return proc;

}

As you may have already noticed, the important stuff is in the CreateMasterProcedure method: after verifying that the object doesn't already exist, it proceeds to create it, sets the root folder as its Parent, takes the procedure part (conveniently casted as an IApplyDefaultTarget), sets that part as default, and sets "CreateProcedureCalls" as the metadata for our default provider.

Testing the results so far

We've just finished creating and arranging all the pieces involved in the use of a default provider, so it's a good time to do a test run and verify that everything is working as expected. If you run GeneXus at this point and open any KB, you should find a procedure MasterProc in the root folder. Open this object and verify that in the Procedure part, you have that comment line we put in the template file.

FirstTest

Generating the calls

Now that we have all the bits connected and working, we can focus in the actual generation of our default. As we have stated above, we want to make a call to every procedure with no parm rule. We might let the template file figure this list out by itself, but we will send the template an enumeration that already resolves this instead. This will also serve to show how to pass additional parameters to the template.

Edit the DefaultProvider.cs file and add the following using statements:

using System.Collections.Generic;

using Artech.Genexus.Common.Objects;

Also add the following method to the DefaultProvider class:

private IEnumerable<Procedure> GetProceduresToCall(IApplyDefaultTarget target)

{

   foreach (Procedure procedure in Procedure.GetAll(target.Object.Model))

   {

      bool hasParms = false;

 

      foreach (Signature signature in procedure.Rules.Signatures)

      {

         hasParms = true;

         break;

      }

 

      if (!hasParms)

         yield return procedure;

   }

}

Finally, add the following line to the AddParameters method:

parameters.Properties.Add("Procedures", GetProceduresToCall(target));

Since we are passing a new parameter to the template, we need to update the template file so that it can receive and use it. Open ProcedureCalls.dkt and add the following line right after the declaration for the Part property at line 6:

<%@ Property Name="Procedures" Type="IEnumerable<Procedure>" %>

Also, add these lines inside the CDATA section (ie: at line 11):

<%foreach (Procedure procedure in Procedures)%>

<%{%>

   <%if (!procedure.Equals(Object))%>

   <%{%>

 

// <%=procedure.Description%> - Last update: <%=procedure.LastUpdate.ToString()%>

<%=procedure.Name%>.Call()

   <%}%>

<%}%>

The code in the template iterates through the Procedures property and adds a call to each procedure in the output (unless it is the same object for which it's generating this source code). Before each call, it also generates a comment line stating the object description and its last update time.

We are almost there

Just hit F5 again and open the MasterProc. The Procedure part should now show a call to each of the objects in the knowledge base that match our selection criteria, if any.

SecondTest

However, there is a little problem with our solution so far: if you try to delete any of the procedures that our default part is calling, GeneXus will refuse to delete it on the grounds that our MasterProc object is calling it.

CannotDelete

This is the normal and desired behavior in the general case: when object A has a reference to object B, we don't want to allow the user to delete object B because that would leave object A in an inconsistent state. If we take a look at the references for our MasterProc object (Ctrl+F12), we'll see the reference to each of the called objects, and none of them can be deleted.

HardReferences

But our case is different, because the calls are the results of a default, and any time the calling object is opened again, it will recalculate its source code and everything will be fine (ie: it won't find the deleted object so it won't generate a call for it anymore). So, there's no need to prohibit deleting those objects just because of our calls; we would rather let the user delete any called object and let the default take care of the rest. That's why default providers have the chance to modify references according to their needs.

Edit DefaultProvider.cs again and change the UpdateReference method to the following implementation:

public virtual bool UpdateReference(IApplyDefaultTarget target, EntityReference reference)

{

   reference.ReferenceType = ReferenceType.Weak;

   return true;

}

This method is called for every reference that is the result of a calculated default, so that the default provider can make any change it needs. With this new implementation, we are changing the reference type to Weak (ie: one that doesn't imply the user can't delete the referenced object), and returning true to indicate we did make a change.

Time for a final test

Although defaults are recalculated each time we open an object, its cross-reference information is updated only when we save it again. The references from MasterProc to the objects it's calling were already saved as Hard, and in order to change them we simply need to save the object again. Run GeneXus one more time, open MasterProc and select the Create Default option in the Edit menu. This will calculate the default exactly as it was calculated before but it will serve as a way to mark the object as modified and allow us to press Ctrl+S and save it again. If you look again at the references for MasterProc, you'll note the names of the objects it references are written in italics, which is the way to show they are weak references.

WeakReferences

You should now be able to delete any of those procedures (unless it's being referenced anywhere else, of course), and the next time you open MasterProc, there won't be a call for that object anymore.