views:

209

answers:

4

Background

Weekly we will get some users calling out for help why thy can't do X on form Y. Because of complex business rules, we often have to revert looking in the code ourselves to know why that particular action is not available at that time. Are there any proven strategies to deal with this?

How does one collect all the information from the GUI, business rules and/or security that leads to disabling a button?

Example

The user can't remove a measurement from the measurement overview form because

GUI

  • there is no measurement selected in the form.
  • there are multiple measurements selected in the form.

Business rules

  • the selected measurement has been used in a calculation.
  • the selected measurement is linked to (what we call) a productfiche.

Security

  • the current user is not a member of the group of analysts responsible for that particular measurement.
  • the current user is not an analyst.

Edit

Regarding the valid comments about the fact that we've already made a calculation to decide if the control should be disabled.

We use a home brew ACL to handle security. These are the steps to decide if a control should be disabled

  • A Global ACL is retrieved (currently from a database). If a Write ACE in the ACL for the measurement property is present, this indicates that the current user has the right to change the measurement.
  • A measurement business object gets a copy of this global ACL. The business object puts his business rules on top of the retrieved ACL. If the business rules dictate that the measurement should not be writable, it adds a Deny Write ACE to the ACL.
    Note that a business object can only make the security more restrictive. If global security dictates it can't be done, it can't be done.
  • The coupling with the business object's ACL and the GUI is done through what we call our GuiMap object. This object retrieves a copy of the ACL from the business object and allows the developer to add function pointers returning booleans to it to add Gui rules on top off the business objects ACL.

Now to determine if a button should be enabled, the GuiMap evaluates every function passed to it in combination with the security determined by the ACL of the business object and in extremis with the security of the user.

  • If the user has no rights, the result is always disabled.
  • else if the business rules say it should be disabled, it is disabled.
  • else if any one Gui rule says it should be disabled, it is disabled.

So in fact, every layer builds on top of the previous to determine the final outcome. It is not like there will be one calculation to determine if a button or anything else should be enabled.

The beauty if you like is this: as ACL's hand out copies, the copies attach themselves to the master and get notified when their master ACL get's updated. This allows us to

  • let every control get updated if a users logs off/on in anyone screen.
  • let every control get updated on a change in the business object demands it.

This works quite well for us, except that it is hard to know why something is disabled.

+1  A: 

I would always visibly disable the control and let the user know the reason either by a hover hint or by a dialog when clicked (if your GUI allows to listen to events on disabled controls).

Gipsy King
@Gipsy King - I have updated the question to (hopefully) better reflect what I am asking. In essence, I'd like to know how you know the reason(s) at runtime.
Lieven
@Lieven Surely if you've made a calculation to decide the control should be disabled in the first place, you then know that reason?
roryf
@Rory - You are right, I have updated the question with explaining the problem(s) I have with this.
Lieven
@Lieven At the moment when you set a control to disabled, you could pass a disabled-reason text, to hint the user.
Gipsy King
@Gipsy King - it would be relatively easy to know *when* we disable a control, but in our current implementation, it still would not be possible to know the *why*. I have updated the question to better reflect this.
Lieven
@Lieven I am not sure if I understand correctly. Shouldn't you be able to know *which* rule triggered the deactivation?
Gipsy King
@Gipsy King - it would require some serious hacking and refactoring in its current implemenation. As far as global security is concerned, the user may or may not. As far as a business object is concerned, it just updates an ACE depending on its own rules. As far as the Guimap is concerned, it just looks at the result of all the functions passed through it. The manager of all this if you like (wich is the GuiMap btw) can only ask the business object if the property is writable or not, not *why* it is writable or not, nor should it have to care.
Lieven
+1  A: 

If it's not explained in the documentation and it's not clear on the GUI, I see no alternative to having to look into the code. I've done this a thousand times, so I'm really interested in any alternative posted here :)

Maybe we could write the reason to the logs every time the button is disabled/enabled. For example:

  • Disabled button A because user does not have security privilege ABC
  • Enabled button B because all required information is now available
Bruno Rothgiesser
don't you just hate documentation... who reads that anyway :)
Lieven
+2  A: 

If I understand you correctly, you have two separate problems:

1) The checks in each layer just return a boolean indicating enabled / disabled, and don't return the reason.

You would have to change that such that each check also returns the reason; e.g. you could return a tuple (enabled, reason), where access is the boolean that you have now, and reason a description of the reason why it is disabled (e.g. as a string).

Depending on the environment you are working in, changing the return type of all access checks may not be feasible; if you want to avoid this, you could report the reason "out of band", e.g. stash it into a global (or rather thread-local) variable, a la errno in UNIX or GetLastError in Windows. That's not that elegant, and will certainly be frowned upon by many, of course :-(

Alternatively, you could change your checks to throw exceptions (with a descriptive message) instead of returning a boolean. Again, this will be frowned upon in many environments...

2) Your business layer adds entries to the object's access control lists. When you later check the access, you know which entry denied the access, but you don't know anymore, why this entry has been added.

Th solve this, you could add a reason field to the ACEs. When the business layer adds an ACE, it sets the reason to a description of the reason why the access was denied. The access control check then reads the reason from here and passes it up to the GUI layer as described above.

oefe
+1. This is exactly what I think I would have to do to solve the problem. Problem with this is that I have to carry the 'reasons' everywhere, adding a substantial overhead even when I don't need them.
Lieven
What kind of overhead are you concerned of? I don't think that processing overhead will be an issue.You can't entirely avoid the programming overhead, but with a careful design, you should be able to keep it relatively low. E.g. depending on the programming language used, you design the result tuples such that you can still use them like booleans when you only need this (e.g. override `operator bool`). Anyway, most of the overhead will be producing the reasons, and this is unavoidable.
oefe
I am in complete agreement here but I wished it would have been possible to come up with a system where the reasons would only be produced when asked for, not all the time. It would be possible by adding a list of function pointers to the ACE wich could be looped over externally to get the reasons but even adding and housekeeping this list would add more overhead than I'd like. Ideally in my p.o.v., an external object should be able to take a control as input, work out wich ACE's and bussiness rules are used in concert en work out a reason from that.
Lieven
+1  A: 

Lieven, I would personally recommend you solution given by oefe.

.

However since you are not satisfied I am adding my different approach below. This will work only if your GUI screen rendering process is idempotent (i.e. will not change state of system and can be invoked multiple times without affecting the system).

  1. Using decorator pattern create a wrapper rules that can enclose your original business rules. Wrapper rule will execute original rule as well as add the reason if access is denied.
  2. Use strategy pattern to get instance of rules. Depending on parameter, strategy should be to use original business rules or wrapped business rule.
  3. On client screen when disabled control is clicked, asynchronously invoke same process that was used to originally construct the screen. However this time add extra parameters, i.e. control's id and reason indicator that you want to know reason why respective control is disabled.
  4. When reason indicator parameter is present, use strategy that uses wrappers. This wrappers add reasons to ACE. At the end, for given control, retrieve the reason for required control id and return it.
  5. When client get's reason as asynchronous response, display it.

Disadvantages:

  • Screen initiation has to be idempotent
  • More work to have wrappers for rules
  • More work on how rules are instantiated (use of strategy)
  • If rules are not idempotent or cannot be used in wrapper, you will be required to create parallel hierarchy of rules.

Advantages:

  • Reasons are not populated by default
  • When user requests for a reason, only then extra processing is done.

.

Update:

Alternatively for crude but more simplistic approach, you can incrementally one module at at time, refactor existing rules and make them sensative to control's id and reason indicator parameter. If reason indicator parameter is present rules itself should populate the reason.

Gladwin Burboz
+1. Phew, need to dome some serious refactoring but essentially, it would do the job.
Lieven