Adding Drag/Drop Reorder to ArcGIS Flex Viewer TOC

Wow, that is a long, clumsy title.  Google will like it, though.

Anyway, I’ve been neck-deep in the world of Flex, focusing on the ArcGIS Server Flex API the last couple of months.  We delivered a map viewer based on Flex to a client recently, and it was a great learning experience.  ESRI released a Sample Flex Viewer component that includes the Table of Contents (TOC) sample from the code gallery.  The TOC component was originally built by Tom Hill at ESRI.  He and I have traded a couple of e-mails about adding drag and drop reorder functionality, so I thought I’d run through the basics of what it takes to get it working.

Download the Flex Viewer

If you haven’t already, download the flex viewer from here.  Extract the sample SOURCE (there are two zips, binary and source…we are using the source archive)  to your development area.  I am using FlexBuilder, so my life is easy, as I can just import the existing project into the IDE.  You don’t have to have FlexBuilder, but I am going to presume you know how to edit source files and compile the project.

Modify the LiveMapsWidget

The TOC component is used by the com.esri.solutions.flexviewer.widgets.LiveMapsWidget MXML component.  Open the source for that bad boy (the namespace==the directory).   Find the TOC component, which looks like

<toccomp:TOC id="toc" width="100%" height="100%"/>

We want to enable dragging on the component.  The TOC component extends the core Flex Tree component, so enabled drag drop is as simple as adding

dragEnabled=true

to the element.  If you do that and the run the viewer, you’ll notice you can open the Live Maps widget (in the Globe menu)  and drag the “Lousiana Landbase” layer around.  It doesn’t do much, but you can drag it.  In fact, you get a rex “X” while you drag, which means you can’t drop it anywhere.  So, how do we get rid of that pesky red “X”?   We have to tell the DragManager that the TOC will accept a drag/drop.  The DragManager is a Flex class that maanged drag and drop operations.  Google it for more info (sorry, but I have to constrain the post ot it will go War and Peace on me)   The time to tell the DragManager about the openness of the TOC is when something is dragged (?  drug?) over it.  We do this with the dragEnter event.  So, adddragEnter

dragEnter="onDragEnter(event)"

to the toc element.  Now we have to write the onDragEnter  method.  In the mx:Script area of LiveMapsWidget.mxml, copy this code:

			private function onDragEnter(event:DragEvent):void{
				DragManager.acceptDragDrop(event.currentTarget as TOC);
				if ((event.currentTarget as TOC)==null)
				{
					DragManager.showFeedback(DragManager.NONE);
					event.preventDefault();
				}

			}

This function fires when we drag something over the TOC and tells it that we are open for business. Business is, of course, things that can be dragged and dropped. You could put more logic in here to filter out non TOCItems, etc, but that is left as a lesson for the reader.

OK,  so our red “X” is gone over the TOC (but still there if you drag a layer over the map, cool!)  but now we want it to do something when we drop the layer.  Specifically, we want it to reorder the layers.  But we only have 1 layer, so let’s quickly add another.  Open up the config.xml in the src directory and add the following tag to the <livemaps> element:

<mapservice label="Louisville Public Records" type="dynamic" visible="false" alpha="0.4">http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Louisville/LOJIC_PublicSafety_Louisville/MapServer</mapservice>

Now we have two layers, making a drag/drop scenario much more compelling.  If you run the viewer you will see both our layers in the LiveMapsWidget.   Now, just like we had to tell Flex to enabled dragging on the TOC, we have to tell it to enabled dropping.  You guessed it, add

dropEnabled="true"

to the toc element.  Now, when you drag/drop a layer, they will reorder in the TOC, which is nice.  The map layers don’t do anything, but we’ll get there.  We have a problem now, though.  The Tree component makes no distinctions between services (roots, in this case) and the layers that make up those services.  This results in the ability of the user to be able to drop a map service under another map service, which is bad.  You can do a lot of checking when the drop occurs to make sure that the user hasn’t dropped one service as a child of another.  I am gonna go an easier route and collapse all the roots (map services) on drag start so those crafty users can’t do it.  They may not like it, but life is sometimes hard.  So, let’s add a dragStart function:

(on the toc)

dragStart = "onDragStart(event)"

(the function)

			private function onDragStart(event:DragEvent):void{
				//Close the dragged item
				_draggedLayer=toc.selectedItem  as TocMapLayerItem;
				var openItems:Array = toc.openItems as Array;
				for each(var o:Object in openItems){
					toc.expandItem(o,false);
				}
				//setInfoText(resourceManager.getString("resource","toc-layer-reorder"));
			}

K…we’re getting there.  Now to the meat of what needs to happen.  When we drop the service, we need to calculate it’s new index in the map, then tell the map to reorder the services.  It took me awhile to find an algorithm that worked for this, and here is why:

  • The basemaps.  The TOC doesn’t have the basemaps, so you have to take that into account when calculating the new index.
  • If the layer is dragged down, then you have to account for that.
  • The TOC indices are reversed form the map services.  So index 0 in the map is the lowest, but it’s the topmost node in the TOC.

So, nothing overwhelming, but details.    Let’s add the event to the TOC element:

dragComplete = "onDragComplete(event)"

and the code:


private function onDragComplete(event:DragEvent):void{
 if (event.action!="move")
 return;
 var _roots:ArrayCollection = toc.dataProvider as ArrayCollection;
 //Unclear why I have to do this....but I need the selectedIndex later
 toc.selectedItem = _draggedLayer;
 var dropIndex:uint=toc.calculateDropIndex(event);
 // I've seen thie calculated drop index be > than the number of 
 // services.  This usually happens when a service node is expanded,
 // but let's just make sure.  We'll put it at the bottom of the list 
 // in this cse.
 if (dropIndex>_roots.length)
 dropIndex=_roots.length;

 var ind:int=0;
 // Set in onDragStart....if it's null, get outta dogdge
 if (_draggedLayer==null)
 return;
 var delta:int = _roots.length-dropIndex;

 ind = delta + 1;//We have two base layers....HACK...THIS IS BAD, MAKE IT BETTER

 // If the selected item is dragged down, then the index needs to account for that
 if (toc.selectedIndex>dropIndex)
 ind=ind-1;

 toc.map.reorderLayer(_draggedLayer.layer.id,ind);

 }

The comments in that function go through what I am trying to do.  I’ll be the first to admit that it isn’t the prettiest code at the ball, but it’s the one I brought so I am dancing with it (Note to self:  work on better analogies)

So, you’d expect it to work now, woudln’t you?  Well, it doesn’t.  We have to make a minor change to the TOC to handle the layer reorder.   In the TOC.as (in src\com\esri\solutions\flexviewer\components\toc) the onLayerReorder function looks like:

private function onLayerReorder( event:MapEvent ):void
{
var layer:Layer = event.layer;
var index:int = event.index;

for (var i:int = 0; i < _tocRoots.length; i++) { var item:Object = _tocRoots[i]; if (item is TocMapLayerItem && TocMapLayerItem(item).layer === layer) { _tocRoots.removeItemAt(i); _tocRoots.addItemAt(item, _tocRoots.length - index - 1); break; } } } [/sourcecode]

Modify the TOC Code (Just a little…)

When you run the viewer now, you’ll get RangeErrors on the addItemAt line above.   So, my approach was to calculate the new TOC index by figuring out the difference between the number of layers and the new index.  Then, make sure somethign didn’t go haywire and we are out of range.  See below:

private function onLayerReorder( event:MapEvent ):void
{
var layer:Layer = event.layer;
var index:int = event.index;
//How far did we move?
var addbackind:int=(map.layerIds.length-1) – event.index;
for (var i:int = 0; i < _tocRoots.length; i++) { var item:Object = _tocRoots[i]; if (item is TocMapLayerItem && TocMapLayerItem(item).layer === layer) { _tocRoots.removeItemAt(i); // If we are out of range on the high end, rein it in if (addbackind>_tocRoots.length)
addbackind=_tocRoots.length-1;
// If we are out of range on the low end, rein it in
if (addbackind<0) addbackind=0; _tocRoots.addItemAt(item,addbackind); break; } } } [/sourcecode] May not be the prettiest...etc, etc.  But it works.  You should now have drag/drop reoder working like a champ.  Obviously, this code could still be improved.  The biggest example is accounting for the basemaps in a cleaner fashion.  I will tell you that we used the Specification Pattern to determine if a service was a basemap, allowing the TOC to ask the specification.  I liked that, but didn't include it here to try and keep this post focused. Anyway, try it out and see how it goes.  If you have improvments or suggestions, hit me in the comments.  Here is a link to the final LiveMapWidgets.mxml I used for this post.

Advertisements

About Ruprict

I am a nerd that is a Nerd Wannabe. I have more kids than should be allowed by law, a lovely wife, and a different sense of humor than most. I work in the field of GIS, where I am still trying to find myself on the map. View all posts by Ruprict

14 responses to “Adding Drag/Drop Reorder to ArcGIS Flex Viewer TOC

  • George

    I’m standing up and clapping. You just saved me a whole hell of alot of development effort. THANK YOU!

    I just tried it out with two layers, a dynamic map service and an image service, and it worked flawlessly.

    Thanks,

    -George

  • Craig Carns

    Thanks for the post I am using it my Flex Viewer now! 🙂

  • Bill

    Thanks for the tips…I’m kind of new to this, but do I edit the LiveMapWidgets.mxml? And what other files do I need to edit…if so can you post the entire code for each file update….

    Thank you so much for you time,

  • ruprict

    I blew away the code I was using for this blog, so I’ll have to redo it, which may take me awhile to get to.

    There is a change to the LIveMapWidgets.mxml, which is the only file I posted. (linked toward the end of the post)

    I’ll try to get another version going….

  • Paul

    Hi Glenn,

    Thanks for the great post on enabling drag and drop in the livemap widget. I’ve been using it in a map project I’ve been working on at my university. I just updated my map application to the latest version of the Sample Viewer and made the alterations to the LiveMapsLayer widget as in your blog. This modification worked great with the old version of Sample Viewer but with the update I seeing some errors in how the layers are handled. In the old version layer names were the same for both the Layer Visability and Layer Transparency views. Now for Layer Transparency I’m seeing names like ArcGISDynamicMapServiceLayer93 and ArcGISDynamicMapServiceLayer94. I’ve been comparing the xml and mxml for the old and new versions of the widget, but I’m finding no differences. Drag and drop seems to be working in the layer visability view, but it’s inconsistent with what the map displays. Can you suggest where this is happening? You can see a live version of my map application at the included link.

    cheers, Paul

  • beanwl@gmail.com

    Paul,

    Check this ESRI post out: http://forums.esri.com/Thread.asp?c=158&f=2421&t=289612&mc=3#msgid901417

    this will fix the naming issue

  • Paul

    Thanks Glenn,

    That solved the problem.

    cheers, Paul

  • Neil Devadasan

    Thank You.

    I had to include following lines too.

    import com.esri.solutions.flexviewer.components.toc.tocClasses.*;
    import mx.managers.DragManager;
    import mx.core.DragSource;
    import mx.events.DragEvent;

    private var _draggedLayer:TocMapLayerItem;

  • Jason H

    I am new to the Flex world and was wondering if anybody has adjusted this script to work with headings and subheadings. Currently, when I move a layer, it takes it to the main headings and I would like it to stay within the that subheading.
    So you could move (See below) Single Lane over freeway only, but Roads could go over Parks, but neither Parks or Roads could be moved out of Base Maps.

    eg.
    Base Maps
    Parks
    National Parks
    Local Parks
    Roads
    Freeway
    Single Lane

  • Jason H

    Okay, my post did not format right. Think of Base Maps as the top most heading, then Parks and Roads a subheading, then National/Local Parks under Parks and Freeway and Single Lane under Roads.

  • Jason H

    Has anybody used headings, subheadings, and sub sub headings. As an example, think of Base Maps as the top most heading, then Parks and Roads are a subheading, then National/Local Parks under Parks and Freeway and Single Lane under Roads. Single Lane or Freeway could move only under Roads, while National/Local Parks could only move under Parks. Although Roads or Parks could move up or down, but not out of the Base Maps heading.
    I am new to Flex and right now any movement is into the main headings area.

  • Allison Shaw

    Hi Ruprict,

    I love this functionality! Does it work for Flex API 2.4 too, or have you developed a workaround for this version?

    Thanks,
    Allison

    • Ruprict

      Hey Allison,

      Unfortunately, I am not working on anything GIS or ESRI related right now. I have not tried to port this to the latest ArcGIS API, and I probably won’t, just b/c I am out of GIS right now.

      Be nice if someone did, though..hint..hint 🙂

      • Allison Shaw

        Thanks for getting back to me. I found out that version 2.5 of the API has this capability builty, so I just need to wait for my organization to make the switch.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: