Extending Legend Control

8111
8
03-16-2011 10:24 AM
DavidHollema
New Contributor III
I'd like to organize items (layer items) in the legend control into headings, for example SL headered items or accordion items.  I have the need to categorize dynamic map service layers according to categories that I retrieve from another web service source.  It doesn't appear that the legend control currently offers the capability of tagging or categorizing its layer items.


  • What are some ideas for the least painful way to accomplish this?  I have 2 thoughts but would like to hear other approaches.  Ideally I would subclass from LayerItemViewModel and introduce a new property, "Category", for example but that class is sealed.  And I don't want to modify ESRI source code if it can be prevented.

    1. Somehow tag a layer item with a category and use this for categorization.

    2. Intercept legend control refresh events and perform my categorization at that point with my own solution in code.  Don't know if Legend.Refresh event is intended for this purpose.


I chose to extend the control by deriving from it and creating my own control.  My intent was to avoid modifying ESRI's toolkit source for maintainability reasons.  With my derived categorized legend control, my goal was then to intercept any Legend.Refresh events and perform the categorization myself in code in my new control.  My control template headered items bind to the categories I created.  I ran a problem with this approach and have some questions.


  • I downloaded legend control source and spent some time reading code and generating object model diagrams.  I am trying to wrap my head around the object model, specifically what real-world entities (abstract data types) the layer item, legend item, map layer item, etc. classes represent.  The object model seems very slick but somewhat confusing in its complexity/recursive nature.  Is there any conceptual level help documentation for toolkit controls beyond the auto-generated stuff at http://help.arcgis.com/en/webapi/silverlight/apiref/api_start.htm  Any help even via this forum dialog would be much appreciated.

  • The Legend.Refresh event does not fire for all cases as I would expect.  My expectation/hope was that whenever any layer was added/removed from the map, the event would fire.  I'm using the MVVM pattern.  My layer collection is data bound to the map in xaml, my map is data bound to the legend control, and my layerID list is data bound to the legend control to limit which layers appear in the legend control.  I spent time debugging and watching when that Refresh event fires.  It doesn't fire for all additions or removals of a map layer or change in layerID list.  I reverted then to registering for notification of when the Legend's LayerItemsSource dependency property changes which isn't ideal.

I recognize that this is a long post and am hoping to just start some dialog with one of the developers of this control.

Thanks,

Dave
0 Kudos
8 Replies
DominiqueBroux
Esri Frequent Contributor

I'd like to organize items (layer items) in the legend control into headings, for example SL headered items or accordion items.


Here is an example of legend control template allowing to organize items into accordion. With this template, there are 3 levels of items (the group layers are not displayed):
1) map layer (e.g. dynamicmap service)
2) SubLayers which are not group layers
3) Label+Swatch

<Style x:Key="Accordion" TargetType="esri:Legend">
<Setter Property="LayerItemsMode" Value="Flat" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="esri:Legend">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
                               Background="{TemplateBinding Background}"
                               BorderBrush="{TemplateBinding BorderBrush}"
                               BorderThickness="{TemplateBinding BorderThickness}"      
                               >
<toolkit:Accordion
                               Background="{TemplateBinding Background}"
                               Foreground="Black"      
                               ItemsSource="{TemplateBinding LayerItems}"
                               HorizontalAlignment="Stretch"
                               SelectionMode="ZeroOrMore">
<toolkit:Accordion.ItemTemplate>
<DataTemplate >
<!-- Map Layer legend item (1st level in legend hierarchy)-->
<ContentPresenter Content="{Binding}" ContentTemplate="{Binding Template}" />
</DataTemplate>
</toolkit:Accordion.ItemTemplate>
 
<toolkit:Accordion.ContentTemplate>
<DataTemplate>
 
<ItemsControl ItemsSource="{Binding LayerItemsSource}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- Layer Item or Legend Item (2nd level in legend hierarchy)-->
<ContentPresenter Content="{Binding}" ContentTemplate="{Binding Template}" />
 
<ItemsControl ItemsSource="{Binding LegendItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!--Legend items (3rd level = last level due to flat option)-->
<ContentPresenter Content="{Binding}" ContentTemplate="{Binding Template}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
 
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
 
</toolkit:Accordion.ContentTemplate>
</toolkit:Accordion>
</ScrollViewer>
</ControlTemplate>
 
</Setter.Value>
</Setter>
</Style>

Note : the default template is using a treeview so the number of items levels is not limited and we can display group layers inside group layers inside .....
I have the need to categorize dynamic map service layers according to categories that I retrieve from another web service source. It doesn't appear that the legend control currently offers the capability of tagging or categorizing its layer items.
From 2.2, a tag property has been added to the LegendItemViewModel class. This property is designed for the scenario you are talking about (you could store your category in this property).


Ideally I would subclass from LayerItemViewModel and introduce a new property, "Category", for example but that class is sealed.

Yes, but this would not be that simple because without legend extensibility mechanisn, your class would not be automatically instantiated by the legend control so you would have to do it by code anyway.


Somehow tag a layer item with a category and use this for categorization.
Intercept legend control refresh events and perform my categorization at that point with my own solution in code. Don't know if Legend.Refresh event is intended for this purpose.

Looks good. The best is you to use the version 2.2 and the new tag property. For previous version, the layer tag can be an acceptable workaround.
The refresh event is intended for this purpose, i.e. modify, add, delete legend items returned by the map services.

0 Kudos
DominiqueBroux
Esri Frequent Contributor
Is there any conceptual level help documentation for toolkit controls beyond the auto-generated stuff at http://help.arcgis.com/en/webapi/sil.../api_start.htm Any help even via this forum dialog would be much appreciated.

With the source, you get a legend diagram class (legend.cd) which could help.



In short, the main classes are:

  • LegendItemViewModel : Base class for a legend item


    • a label + an image

    • a template to display the item

  • LayerItemViewModel : extent the LayerItemViewModel to represent a layer or a sublayer in the legend


    • Additional properties : Layer, SubLayerID, LayerType, MinimumResolution, MaximumResolution, IsInScaleRange).

    • 'LayerItems' : returns the collection of layer items at the next level of the legend hierarchy (e.g from a map layer item, you get the group layers and/or the sublayers, from a group layer you get the sublayers and/or the sub group layers)). Here is the recursivity.

    • 'LegendItems' : Low level legend items under the layer item.

  • Legend : Control itself.


    • 'LayerItems' : returns the collection of layer items at the first level of the legend hierarchy (i.e. items corresponding to the map layers).

    • 'LayerItemsSource' is a kind of view on the legend items hierarchy taking care of the 'ShowOnlyVisibleLayers' option, and of the Flat/Tree mode.

The Legend.Refresh event does not fire for all cases as I would expect. My expectation/hope was that whenever any layer was added/removed from the map, the event would fire.

Not exactly but close. The event is fired each time a first level layer item (i.e a layer item representing a map service) has been refreshed.



This happens:

  1. after adding a layer: as soon as the legend is available (we get the legend asynchronously).

  2. when the legend of the map layer has changed (e.g. if the renderer of a feature layer changes).

  3. when someone calls 'Refresh'

Concerning the point1, note that there was a (kind of) bug in 2.1, the refreshed event was not fired with the layers that don't support the legend (such as bing map layers or GPResultImageLayer). This had sense since the legend is not really refreshed but nevertheless we decided to fire the event from 2.2 so one could use the event to add by code the legend infos or remove by code the legend item.
�??

It doesn't fire for all additions or removals of a map layer or change in layerID list. I reverted then to registering for notification of when the Legend's LayerItemsSource dependency property changes which isn't ideal.

�??
Map.Layers and Legend.LayerItems being ObservableCollections, the events fired by these collections should cover many scenarios. Let us know if you think something is missing.


I recognize that this is a long post and am hoping to just start some dialog with one of the developers of this control.


Long question : long answer. I got this error during my first try:mad: :

The text that you have entered is too long (13167 characters). Please shorten it to 10000 characters long.

So I have had to answer in 2 parts.

Hope this help.:)
0 Kudos
DavidHollema
New Contributor III
Thanks for the quick response.  More questions.

None of the notification mechanisms from Legend (Refresh event, LayerItems, LayerItemsSource property changed) do exactly what I want.  I'm looking for a way to be notified when I 1)add a layer and 2) remove a layer.  I'm changing this layer collection in code behind (architecturally speaking it's done in the view model and service layer) and the map is data binding to this collection.  I checked and Refresh event does not fire when I remove a layer from this collection although the map and the legend control do get updated.  I see that you confirmed that with your list...

This happens:

  1. after adding a layer: as soon as the legend is available (we get the legend asynchronously).

  2. when the legend of the map layer has changed (e.g. if the renderer of a feature layer changes).

  3. when someone calls 'Refresh



Dependency Properties approach.  The problem with LayerItems dependency property is it's not updated consistently enough (I know that's vague but it's been a couple weeks since I looked at it).  The problem with LayerItemsSource dependency property is it's updated too often.  For example, I apply a double click action to zoom into the map based on a layer extent.  These dependency property change notification occurs at that point even though I'm not adding or removing layers.  I bet it's because the scale has changed and legend is refreshing scale dependencies.

I believe that the Refresh event would be ideal for me to listen to if it were fired when a layer was removed from the map.  Can you consider adding this?

Next question.  You gave an excellent explanation of 3 of the classes.  They make sense.  I'm still confused about the 2 biggie properties on the Legend control, Legend.LayerItems versus Legend.LayerItemsSource.  I'm using the control in a flat fashion (no tree structure). In that case, is Legend.LayerItems identical to Legend.LayerItemsSource? 

Last question.  What is the purpose of composing your Legend control with that LegendTree class?  I'm trying to understand/curious about why that additional class member was introduced into the design.  I guess that's the one confusing me most since it is also a type of LayerItemViewModel.  The cyclic nature in the class design is what's confusing me -- LegendTree is a LayerItemViewModel and Legend has a LegendTree and LegendTree has LayerItemViewModels and Legend has LayerItemViewModels, etc.
0 Kudos
DominiqueBroux
Esri Frequent Contributor
None of the notification mechanisms from Legend (Refresh event, LayerItems, LayerItemsSource property changed) do exactly what I want. I'm looking for a way to be notified when I 1)add a layer and 2) remove a layer.

LayerItems property changed should do the trick (see below).

The problem with LayerItems dependency property is it's not updated consistently enough (I know that's vague but it's been a couple weeks since I looked at it).

I am interested to get more details about the issue.
In Silverlight the difficulty is to be notified when a DP changed but the following code seems to notify me when a layer is added or removed from the legend (either by changing the map layers or by changing the LayerIds property).
private void MyLegend_Loaded(object sender, RoutedEventArgs e)
{
    RegisterForNotification("LayerItems", sender as Client.Toolkit.Legend, LayerItemsChanged);
}
 
void LayerItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    Debug.WriteLine("LayerItems changed count = {0}", (d as Client.Toolkit.Legend).LayerItems.Count());
}
 
/// Listen for change of the dependency property  
public void RegisterForNotification(string propertyName, FrameworkElement element, PropertyChangedCallback callback)  
{  
    //Bind to a dependency property  
    Binding b = new Binding(propertyName) { Source = element };  
    var prop = System.Windows.DependencyProperty.RegisterAttached(  
        "ListenAttached"+propertyName,  
        typeof(object),  
        typeof(UserControl),  
        new System.Windows.PropertyMetadata(callback));  
 
    element.SetBinding(prop, b);  
}

The problem with LayerItemsSource dependency property is it's updated too often.

You are right. The hierarchy of the legend items can be depending on the scale and so can change when zooming.

I believe that the Refresh event would be ideal for me to listen to if it were fired when a layer was removed from the map. Can you consider adding this?

The meaning of this event is not 'The legend has been refreshed' but 'One layer item (given as arg) of the legend has been refreshed'.
We could imagine to fire this event as well when a layer item is added or removed, but we should add an info in the eventargs (item refreshed, item added, item removed) and probably rename the event to 'Changed' (an item which is removed is not really 'Refreshed').

(Furthermore in my mind, LayerItems property changed should do the trick)

Next question. You gave an excellent explanation of 3 of the classes. They make sense. I'm still confused about the 2 biggie properties on the Legend control, Legend.LayerItems versus Legend.LayerItemsSource. I'm using the control in a flat fashion (no tree structure). In that case, is Legend.LayerItems identical to Legend.LayerItemsSource?

Not identical.
Legend.LayerItems is always returning the map layer items participating to the legend (i.e. the first level of items in the legend hierarchy even if these items are not displayed in the legend).
Legend.LayerItemsSource in flat mode returns only the last level of layer items (i.e. doesn't return the map service layers nor the group layers).
This property make easier the legend templating by providing the items which have to be displayed.
The default template is using LayerItemsSource but more complex scenario can mix LayerItems and LayerItemsSource. Perhaps you noticed that the accordion sample I gave was using 'LayerItems' for the first level (so all map layer items are displayed in the legend control) and is using 'LayerItemsSource' for the next level (so the group layers are not displayed).

What is the purpose of composing your Legend control with that LegendTree class?

The legend items being organized as an hierarchical tree, we can consider that there is a root node somewhere allowing to walk though the whole tree.
The LegendTree object represents this virtual root node which is never displayed (there is one root node by legend). This object manages the event coming the map and impacting the whole legend tree (layer added, layer removed, scale changed, ...)

This root node implements 'LayerItemViewModel' so all the tree nodes implements the same interface (and mainly LayerItems property which gives the children of the node) and, so, walking though the tree is easier to develop (but for sure this could have been done another way, but anyway LegendTree is an internal class).

Hope this help
0 Kudos
DavidHollema
New Contributor III
The solution that worked for me is as follows.  I listen to the Legend.Refreshed event as well as registering for notification of the Legend.LayerIDs property changed.

//listens for layer added to legend but not removed
base.Refreshed += new EventHandler<RefreshedEventArgs>(BinLegend_Refreshed);

//listens for layer removed from layer id collection
DependencyPropertyUtilities.RegisterForNotification("LayerIDs", this, LayerIDsChanged);


The trouble with registering for change notification on dependency property LayerItems is that at the time of notification, any LayerItems under the specific LayerItem that has been changed (sub-layer items) are null.  In my application, I need to access the sublayer items.

I'm open to hearing any other solutions for accomplishing this.
0 Kudos
AnastasiaAourik
New Contributor II
I have multiple layerIds and by default set ShowOnlyVisibleLayers=true.
I have a checkbox SHOW ALL LAYERS that sets ShowOnlyVisibleLayers to false...
The legend is created SYNCRONOUSLY and user cannot do anything while it is being generated.

I'd like to extend legend control to create it asynchronously.

How Can I do this?
0 Kudos
DominiqueBroux
Esri Frequent Contributor
I have a checkbox SHOW ALL LAYERS that sets ShowOnlyVisibleLayers to false...
The legend is created SYNCRONOUSLY and user cannot do anything while it is being generated.

The legend is not created when setting ShowOnlyVisibleLayers to false.
At that moment the legend items are already created and the LayerItemsSource is just changed to return all layers. Then the UI is automatically updated to reflect that change.

The difficulty to make that part asynchronous is that the UI updates have to be done in the UI thread.
That being said there is probably lot of things that could be optimized.
The source code of the legend is published here and can be tweaked for your need.
I'll be interested in all optimizations that you could get.

Thanks
0 Kudos
BrianFoley1
New Contributor II

Dominique, know this is an old thread, are you still around ?

0 Kudos