domingo, 10 de julio de 2011

Button hell

You've been asked to modify an entity such as that the purchase order or projects tables/forms. Part of the changes implies that you have to disable certain functionality and/or input controls on the form. Hey, it's nice to have this level of detail in the client requirements document isn't it?

First of all we found that adding new fields and related helper tables was easy. It's one of the advantages of Ax 2009 that we're getting quite used to when compared to other frameworks.

Implementing the 'side effects' was a little more strenuous, such as when a project is cancelled we had to apply checks to see if there are any open purchase orders beforehand or we're required to deactivate any associated budget/predicted costs.

Now finally the easy part, deactivating the buttons or sub-menu options on the form... 

Eeek!?

It's not as trivial as you first imagine. And it takes getting used to. Let's take a look at the ProjTable form. We have two methods for updating the buttons and the form input controls, setButtonAccess and setFieldAccess. As the form is so complex they've logically split up the work within setButtonAccess into sub groups such as setButtonInvoice and setButtonFunction. But it's what each one of these then do which takes a little more time to get to grips with. Each method will instantiate the same class and use a method to control the button's enabled or visibility status, thus:
    ProjStatusType projStatusType = projTable.status();
    ;
    ctrlInvoices.enabled(projStatusType.ctrlInvoices());
It's a helper class that we really should familiarize ourselves with and some of its functionality is used to enable or disable the Invoice control, depending upon the project. It's a little complex; however, as we have both project stages and project types to contend with. Here is a simplistic view of these classes:
The ProjStatusType class (which is constructed from a sub class such as ProjStatusType_Created or ProjStatusType_ReportedFinished) instanciates the ProjTableType class which is actually constructed from a subclass such as ProjTableType_Internal or ProjTableType_Cost.  In my brain it took 5 minutes to understand plus another 10 as the Morph X editor is not the most friendly environment to work in.  So when we call the ctrlInvoices() method we're possibly calling a method in ProjStatusType or ProjStatusType_Created or ProjTableType or ProjTableType_Internal, we'll have to go and investigate each class each time we need to change the business logic on each button or input control.  Eeek!  This is exactly what object orientated design is about, however.

When we start, we may just IGNORE the helper class and implement a simple switch statement on the ProjTable_ds.active() method like the pseudo code in the following one and then go for a coffee happy in the knowledge that we've finished ahead of schedule:
    switch (projTable.Status)
    {
        case ProjStatus::Approved :
            CtrlProjValidateSetupEmpl.enabled(projTable.Type != ProjType::Internal);
            CtrlProjValidateSetupCategory.enabled(projTable.Type != ProjType::Internal);
            CtrlPosting.enabled(true);
            CtrlExternal.enabled(true);
            projTable_ds.object(fieldnum(ProjTable, Dimension)).enabled(false);
            break;

        case ProjStatus::PendingApproval :
        case ProjStatus::Created :
            CtrlProjValidateSetupEmpl.enabled(projTable.Type == ProjType::Internal);
            CtrlProjValidateSetupCategory.enabled(projTable.Type == ProjType::Internal);
            CtrlPosting.enabled(false);
            CtrlExternal.enabled(false);
            projTable_ds.object(fieldnum(ProjTable, Dimension)).enabled(true);
            break;

        case ProjStatus::Canceled :
            CtrlProjValidateSetupEmpl.enabled(false);
            CtrlProjValidateSetupCategory.enabled(false);
            CtrlPosting.enabled(false);
            CtrlActivity.enabled(false);
            CtrlExternal.enabled(false);
            projTable_ds.object(fieldnum(ProjTable, Dimension)).enabled(false);
            break;

        default :
            CtrlProjValidateSetupEmpl.enabled(false);
            CtrlProjValidateSetupCategory.enabled(false);
            CtrlPosting.enabled(false);
            CtrlActivity.enabled(false);
            CtrlExternal.enabled(false);
            projTable_ds.object(fieldnum(ProjTable, Dimension)).enabled(false);
            break;
    }
Easy to understand isn't it? Took us a few minutes and no one else ever looks at our code anyway, right?

Problemo, as they say in Spanish.  It's not that maintainable for others, or ourselves in the long term, plus what happens if we have changed the ctrlInvoice button on the ProjTable form but not from other references to the project table.  These classes are used not only on this particular form but all over the place and if we took just a little effort to modify them to the clients requirement we will be pleasantly surprised to find out that we can't create an invoice for a closed project associated with a supplier as we modified the helper class method in that other form.  Less buggy code, and our original ProjTable form is still relatively clean without that switch statement.

Adding a new method to deactivate a button using this technique requires at the very least new methods to be created in two different classes, more so if we are applying business logic to various project types and statuses in use.

Developer tip: Use the ClipX application for those repetitive copy/paste operations. I can also recommend listening to some really racy music to make your life seem 'not so bad' and pass your time away.

Finally another advantage of using this approach is that after we've wired in the class to each button we can recompile the class and change the business logic without having to recompile/restore the original ProjTable form.  Sweet!

And then suddenly it's all broken.

There's an option at the top of the project form to include sub-projects. Our classes and inheritance applied to our buttons is suddenly rendered useless when we have a project that's marked as closed but has a child project that is in progress.... The code just marks the visibility/enabled option as true rather than trying to recurse though the sub-projects checking for their status.  All of our helper classes' architecture aren't wired up to recurse through the child projects.  Bah, humbug.

I also see in the code that the management of the buttons (setButtonAccess) is handled in a different function to that of the enabling/disabling of the fields (setFieldAccess). Fine, but... The two functions are not called from the same parent functions which seems wrong to me. setButtonAccess Is called when we requery the form, change record in the form or change the selected record's status while setFieldAccess called when we change or update the record in the form. I don't see why these to functionalities are launched at different times in the form life cycle.

In conclusion

Many of these complex forms use these helper classes.  After a little effort they do make our life easier and create less buggy code.  Take a little time to learn them and investigate what they do.

No hay comentarios:

Publicar un comentario