Monday, November 17, 2008

Implementing Transfer Observers across all your decorators, the easy way!

In our framework, we use Transfer "Decorators" as a common way to extend the functionality of the generated objects that are based on the configuration XML.

For this system, we have a record (media) that represents each user upload of images. For example, we allow multiple profile images to be uploaded, and you can select your current profile image from the "gallery" of your previous uploads. In addition, however, you can delete these images. If you're familiar with transfer, you know it's as easy to delete the database record as saying oMedia.delete(). That removes the object from the database and from Transfer's cache. In this case, however, we also need to remove the actual files from the webserver.

One possible solution was to create a manager service, and make sure that all media deletions go through it. However, this removes some of the intuitive nature of Transfer providing a delete() function for you to use. With Transfer Observers (or "Transfer Event Model"), you can easily extend your decorators to do this on the normal delete() call.

After my initial creation of a mediaObserver.cfc, which I register with Transfer as an observer, I realized I was going down the wrong path. The problem with this is that all observers registered with Transfer get called on each of these seven events:

observerAfterNew,
observerBeforeCreate,
observerAfterCreate,
observerBeforeUpdate,
observerAfterUpdate,
observerBeforeDelete,
observerAfterDelete


The problem with that? Naming my file MediaObserver has nothing to do with my Media records. The events in the observer get called for any object that is getting ready to be created, updated, deleted, etc. So the first thing I did was to rename the file as TransferObserver.cfc. This is a more generic name, obviously, and reflects more accurately what the file is.

So...the question remains at this point: How do I get it to fire an event specifically for my Media records being deleted? Each of the functions you place in the observer receive a single argument when called: transfer.com.events.TransferEvent

From that event, you can do:

arguments.event.getTransferObject()


This gives you the actual Transfer object being modified for this event. That still doesn't tell us this is the Media object, though. But we can do this:

arguments.event.getTransferObject().getClassName()


That returns, in my case, "media.Media". Now I know that this is the object I want rather than, say, an invoice, product or customer object. So I could now write:

<cfif arguments.event.getTransferObject().getClassName() IS "media.Media">
  <cfset arguments.event.getTransferObject().deleteMyFiles()/>
</cfif>


After writing this, though, it occurred to me that every time I wanted to have a decorator respond to a Transfer event, I would have to modify my TransferObserver.cfc to respond to a new class name. It seemed that I could perhaps do this in such a way that I could just add a function, with a specific naming convention, to each decorator, as needed, and it would just work. And you can. Here is the format now for every function I added to the TransferObserver.

<cffunction name="actionBeforeDeleteTransferEvent" returntype="void" access="public" output="false" hint="I am called BEFORE a Transfer object has been deleted from the database.">
  <cfargument name="event" type="transfer.com.events.TransferEvent" hint="The event object" required="true" />
  <cfset var stLocal = structNew() />
  <cfif structKeyExists(arguments.event.getTransferObject(), "observerBeforeDelete")>
    <cfset arguments.event.getTransferObject().observerBeforeDelete() />
  </cfif>
</cffunction>


What is this? The Transfer observer looks to see if there is a function named "observerBeforeDelete" in the Transfer object (or the decorated Transfer object, since this function naming convention is not part of Transfer's generated file format). If the function exists, it calls it. It's that simple, really.

If you'd like to download the entire TransferObserver file, along with a portion of the Coldspring file showing how to set it up. I've linked it here on my site as a tiny zip file (1.2K). Enjoy!

Just a note: Performing a structKeyExists() on a Coldfusion component, as in structKeyExists(object, "functionName") is not a guaranteed way of ensuring that "functionName" is indeed a function. If you have certain property types (like object.propertyName = true), then that "propertyName" is also a structure key. The correct way is to use the getMetaData() function, then you can ascertain for sure that what you are looking for is actually a function. However, the getMetaData() is a slow performing method and would decrease performance significantly to implement. The structKeyExists() should work just find, because you probably won't have a property of the object using this naming convention. And you can actually use a different decorator naming convention for these event-related functions, too. The cffunction names in the TransferObserver.cfc must, however, remain as in my example.

- Will Belden
November 17, 2008

Tuesday, November 11, 2008

How to reintegrate SVN branch back to trunk using Tortoise

We've begun in our dev shop to use SVN (Subversion)branches to develop, test and implement new features. Like many shops I've run across, we use Tortoise as our SVN client. I didn't fully understand the process of reintegrating my branch back to the trunk, and after some reading (along with some trial and error), came up with the basic steps to get everything back together.

Now, these steps below do not include manually merging any changes. I was fortunate enough in this first exercise to have auto-merge ability for my changes to shared files and all my other files were new.

(Be sure you have any files saved and closed, like edits in Eclipse.)

1. Commit your branch back to the repo.
2. IMPORTANT! Export your branch for backup. Quick way: Make sure you can see your desktop, open a window that shows your root branched folder (in my case, "wwwroot"), right-drag it to your desktop, choose "Export here". Ignore this new export folder from here, unless you have problems.)
3. Right click on your branched folder ("wwwroot" again for me), choose "Merge" from the Tortoise submenu.
4. On the Merge screen that comes up, choose the first option: "Merge a range of revisions". Click "Next".
5. Change the "URL to merge from" to the trunk path. Leave the "Revision range to merge" field empty to bring the HEAD changes to you. (This example is assuming that the trunk has received commits after you branched and committed changes to your branch.) Click "Next" (or test merge to see what will happen).
6. Resolve any conflicts you might have, if this applies to your example.
7. You have now brought the latest trunk changes, and integrated them into your branch, bringing your branch up to date. Your branch now contains the most current commits to the trunk, as well as your branch's deviated changes.
8. Now, with the branch containing trunk changes, you need to re-commit your branch to SVN. Do this now.
9. Now, do an update on your branch, just to be sure. You should receive no updates, but this will ensure the metadata is up to date.
10. Next, do a "switch" on your folder, back to the trunk counterpart. This is in in the Tortoise submenu after right-clicking your branched folder. This allows you to merge the changes from your branch into your working copy of trunk, so that you can commit the branch-to-trunk merge back to SVN. (Earlier you brought the latest trunk changes to your branch, the opposite.)
11. After the switch is complete, go ahead and do an update.
12. Right click on your [trunk] folder, choose Merge from the submenu.
13. Choose "Reintegrate a branch" then click "Next".
14. Change, if needed, the "From URL:" dropdown to the branch SVN location.



Tuesday, November 04, 2008

FancyBox, jQuery and z-index

So yesterday I'm working with the jQuery plugin called FancyBox. It's a great little popup gizmo, similar to Lightbox, Thickbox, et. al. But I'm having a problem. In the page I'm working in, I find that we have some crazy high z-index values having been set. One of them is 100,000. Dude. Crazy.

Anyway...so I wrote a small jQuery-based javascript to determine the highest z-index value of all the elements within the page. After some research into the jQuery selectors, I find what I need. Unfortunately, as I return the .css("zIndex") value back from each object, compare it to a running max, I find that in the end the value "auto" is the highest. That's not helpful. I end up finding a simple isNumeric() function I can include, but it only checks the characters in the value against numbers and the decimal point. So I find that, with my routine, 2000 > 100000. Um...that's not right. That lets me know, though, that it's not comparing numbers to numbers, but rather numbers and character values. The parseInt() function solves this.

In the end, here's the basic function I came up with, how to determine the greatest z-index on the page, using jQuery:
<script>
  var zmax = 0 ;
  function buildZMax() {
    $('*').each(function() {
      var cur = parseInt($(this).css('zIndex'));
      zmax = cur > zmax ? $(this).css('zIndex') : zmax;
    });
  }
</script>

With this, you can now call buildZMax() and the var zmax will hold the highest value. Now, that's not all I set out to do. Getting the highest z-index value was the prelude to modifying my FancyBox setups to make sure the FancyBox is on top of everything.

Here's how it ended up looking, though we will modify more today to actually incorporate this functionality directly into the FancyBox code, submit it to the author, and perhaps he'll include it in his next version. But this will get you by:

 <script>
   var zmax = 0 ;
   
   function buildZMax() {
     $('*').each(function() {
       var cur = parseInt($(this).css('zIndex'));
       zmax = cur > zmax ? $(this).css('zIndex') : zmax;
     });
   }

   function goFancy() {
     buildZMax();

     // Get all the outermost fancy_wrap id'ed objects built by the FancyBox calls
     // ...then bump the z-index to whatever the highest we found, plus 1
     $('#fancy_wrap').each(function() {
       zmax = zmax + 1 ;
       $(this).css("z-index", zmax);

       // Get the overlay children in each iteration of the outer
       // ...fancy_wrap, bump it's z-index up.
       $(this).children("div#fancy_overlay").each(function() {
         zmax = zmax + 1 ;
         $(this).css("z-index", zmax);
       });

       // And again for another major child div
       zmax = zmax + 1 ;
       $(this).children("div#fancy_outer").each(function() {
         zmax = zmax + 1 ;
         $(this).css("z-index", zmax);
       });
     });
 });

   $(document).ready(function(){
     $("a#myFancyLink").fancybox({
       'frameWidth': 600,
       'frameHeight': 600,
       'overlayShow': true
     });

     // Call our z-index fixer.
     goFancy() ;

   });
  </script>

Now, something a bit important to note here. You very likely, if you're a jQuery user, have other scripts that might be creating objects, etc. as you go along. It might be beneficial to put your goFancy() call near the end of your document.ready() code to ensure this ends up where you need it. But the buildZMax() function might very well come in handy for other things you might develop with.

Also, while the selectors in jQuery are screaming fast, if you know that you never, ever put z-index on anything but, say, <div> tags, then replace the "*" in the line:
$('*').each(function() {
... with "div" or whatever you apply your z-index'es to. This will make the whole function run faster, obviously.

Post a comment if you're interested in our incorporation to Fancybox code directly.

- Will Belden
Tuesday, November 4, 2008